fullcalendar.js ➔ compareForwardSlotSegs   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 1
1
/*!
2
 * FullCalendar v2.1.1
3
 * Docs & License: http://arshaw.com/fullcalendar/
4
 * (c) 2013 Adam Shaw
5
 */
6
7
(function(factory) {
8
	if (typeof define === 'function' && define.amd) {
9
		define([ 'jquery', 'moment' ], factory);
10
	}
11
	else {
12
		factory(jQuery, moment);
13
	}
14
})(function($, moment) {
15
16
;;
17
18
var defaults = {
19
20
	lang: 'en',
21
22
	defaultTimedEventDuration: '02:00:00',
23
	defaultAllDayEventDuration: { days: 1 },
24
	forceEventDuration: false,
25
	nextDayThreshold: '09:00:00', // 9am
26
27
	// display
28
	defaultView: 'month',
29
	aspectRatio: 1.35,
30
	header: {
31
		left: 'title',
32
		center: '',
33
		right: 'today prev,next'
34
	},
35
	weekends: true,
36
	weekNumbers: false,
37
38
	weekNumberTitle: 'W',
39
	weekNumberCalculation: 'local',
40
	
41
	//editable: false,
42
	
43
	// event ajax
44
	lazyFetching: true,
45
	startParam: 'start',
46
	endParam: 'end',
47
	timezoneParam: 'timezone',
48
49
	timezone: false,
50
51
	//allDayDefault: undefined,
52
	
53
	// time formats
54
	titleFormat: {
55
		month: 'MMMM YYYY', // like "September 1986". each language will override this
56
		week: 'll', // like "Sep 4 1986"
57
		day: 'LL' // like "September 4 1986"
58
	},
59
	columnFormat: {
60
		month: 'ddd', // like "Sat"
61
		week: generateWeekColumnFormat,
62
		day: 'dddd' // like "Saturday"
63
	},
64
	timeFormat: { // for event elements
65
		'default': generateShortTimeFormat
66
	},
67
68
	displayEventEnd: {
69
		month: false,
70
		basicWeek: false,
71
		'default': true
72
	},
73
	
74
	// locale
75
	isRTL: false,
76
	defaultButtonText: {
77
		prev: "prev",
78
		next: "next",
79
		prevYear: "prev year",
80
		nextYear: "next year",
81
		today: 'today',
82
		month: 'month',
83
		week: 'week',
84
		day: 'day'
85
	},
86
87
	buttonIcons: {
88
		prev: 'left-single-arrow',
89
		next: 'right-single-arrow',
90
		prevYear: 'left-double-arrow',
91
		nextYear: 'right-double-arrow'
92
	},
93
	
94
	// jquery-ui theming
95
	theme: false,
96
	themeButtonIcons: {
97
		prev: 'circle-triangle-w',
98
		next: 'circle-triangle-e',
99
		prevYear: 'seek-prev',
100
		nextYear: 'seek-next'
101
	},
102
103
	dragOpacity: .75,
104
	dragRevertDuration: 500,
105
	dragScroll: true,
106
	
107
	//selectable: false,
108
	unselectAuto: true,
109
	
110
	dropAccept: '*',
111
112
	eventLimit: false,
113
	eventLimitText: 'more',
114
	eventLimitClick: 'popover',
115
	dayPopoverFormat: 'LL',
116
	
117
	handleWindowResize: true,
118
	windowResizeDelay: 200 // milliseconds before a rerender happens
119
	
120
};
121
122
123
function generateShortTimeFormat(options, langData) {
124
	return langData.longDateFormat('LT')
125
		.replace(':mm', '(:mm)')
126
		.replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
127
		.replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand
128
}
129
130
131
function generateWeekColumnFormat(options, langData) {
132
	var format = langData.longDateFormat('L'); // for the format like "MM/DD/YYYY"
133
	format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); // strip the year off the edge, as well as other misc non-whitespace chars
134
	if (options.isRTL) {
135
		format += ' ddd'; // for RTL, add day-of-week to end
136
	}
137
	else {
138
		format = 'ddd ' + format; // for LTR, add day-of-week to beginning
139
	}
140
	return format;
141
}
142
143
144
var langOptionHash = {
145
	en: {
146
		columnFormat: {
147
			week: 'ddd M/D' // override for english. different from the generated default, which is MM/DD
148
		},
149
		dayPopoverFormat: 'dddd, MMMM D'
150
	}
151
};
152
153
154
// right-to-left defaults
155
var rtlDefaults = {
156
	header: {
157
		left: 'next,prev today',
158
		center: '',
159
		right: 'title'
160
	},
161
	buttonIcons: {
162
		prev: 'right-single-arrow',
163
		next: 'left-single-arrow',
164
		prevYear: 'right-double-arrow',
165
		nextYear: 'left-double-arrow'
166
	},
167
	themeButtonIcons: {
168
		prev: 'circle-triangle-e',
169
		next: 'circle-triangle-w',
170
		nextYear: 'seek-prev',
171
		prevYear: 'seek-next'
172
	}
173
};
174
175
;;
176
177
var fc = $.fullCalendar = { version: "2.1.1" };
178
var fcViews = fc.views = {};
179
180
181
$.fn.fullCalendar = function(options) {
182
	var args = Array.prototype.slice.call(arguments, 1); // for a possible method call
183
	var res = this; // what this function will return (this jQuery object by default)
184
185
	this.each(function(i, _element) { // loop each DOM element involved
186
		var element = $(_element);
187
		var calendar = element.data('fullCalendar'); // get the existing calendar object (if any)
188
		var singleRes; // the returned value of this single method call
189
190
		// a method call
191
		if (typeof options === 'string') {
192
			if (calendar && $.isFunction(calendar[options])) {
193
				singleRes = calendar[options].apply(calendar, args);
194
				if (!i) {
195
					res = singleRes; // record the first method call result
196
				}
197
				if (options === 'destroy') { // for the destroy method, must remove Calendar object data
198
					element.removeData('fullCalendar');
199
				}
200
			}
201
		}
202
		// a new calendar initialization
203
		else if (!calendar) { // don't initialize twice
204
			calendar = new Calendar(element, options);
205
			element.data('fullCalendar', calendar);
206
			calendar.render();
207
		}
208
	});
209
	
210
	return res;
211
};
212
213
214
// function for adding/overriding defaults
215
function setDefaults(d) {
216
	mergeOptions(defaults, d);
217
}
218
219
220
// Recursively combines option hash-objects.
221
// Better than `$.extend(true, ...)` because arrays are not traversed/copied.
222
//
223
// called like:
224
//     mergeOptions(target, obj1, obj2, ...)
225
//
226
function mergeOptions(target) {
227
228
	function mergeIntoTarget(name, value) {
229
		if ($.isPlainObject(value) && $.isPlainObject(target[name]) && !isForcedAtomicOption(name)) {
230
			// merge into a new object to avoid destruction
231
			target[name] = mergeOptions({}, target[name], value); // combine. `value` object takes precedence
232
		}
233
		else if (value !== undefined) { // only use values that are set and not undefined
234
			target[name] = value;
235
		}
236
	}
237
238
	for (var i=1; i<arguments.length; i++) {
239
		$.each(arguments[i], mergeIntoTarget);
240
	}
241
242
	return target;
243
}
244
245
246
// overcome sucky view-option-hash and option-merging behavior messing with options it shouldn't
247
function isForcedAtomicOption(name) {
248
	// Any option that ends in "Time" or "Duration" is probably a Duration,
249
	// and these will commonly be specified as plain objects, which we don't want to mess up.
250
	return /(Time|Duration)$/.test(name);
251
}
252
// FIX: find a different solution for view-option-hashes and have a whitelist
253
// for options that can be recursively merged.
254
255
;;
256
257
//var langOptionHash = {}; // initialized in defaults.js
258
fc.langs = langOptionHash; // expose
259
260
261
// Initialize jQuery UI Datepicker translations while using some of the translations
262
// for our own purposes. Will set this as the default language for datepicker.
263
// Called from a translation file.
264
fc.datepickerLang = function(langCode, datepickerLangCode, options) {
265
	var langOptions = langOptionHash[langCode];
266
267
	// initialize FullCalendar's lang hash for this language
268
	if (!langOptions) {
269
		langOptions = langOptionHash[langCode] = {};
270
	}
271
272
	// merge certain Datepicker options into FullCalendar's options
273
	mergeOptions(langOptions, {
274
		isRTL: options.isRTL,
275
		weekNumberTitle: options.weekHeader,
276
		titleFormat: {
277
			month: options.showMonthAfterYear ?
278
				'YYYY[' + options.yearSuffix + '] MMMM' :
279
				'MMMM YYYY[' + options.yearSuffix + ']'
280
		},
281
		defaultButtonText: {
282
			// the translations sometimes wrongly contain HTML entities
283
			prev: stripHtmlEntities(options.prevText),
284
			next: stripHtmlEntities(options.nextText),
285
			today: stripHtmlEntities(options.currentText)
286
		}
287
	});
288
289
	// is jQuery UI Datepicker is on the page?
290
	if ($.datepicker) {
291
292
		// Register the language data.
293
		// FullCalendar and MomentJS use language codes like "pt-br" but Datepicker
294
		// does it like "pt-BR" or if it doesn't have the language, maybe just "pt".
295
		// Make an alias so the language can be referenced either way.
296
		$.datepicker.regional[datepickerLangCode] =
297
			$.datepicker.regional[langCode] = // alias
298
				options;
299
300
		// Alias 'en' to the default language data. Do this every time.
301
		$.datepicker.regional.en = $.datepicker.regional[''];
302
303
		// Set as Datepicker's global defaults.
304
		$.datepicker.setDefaults(options);
305
	}
306
};
307
308
309
// Sets FullCalendar-specific translations. Also sets the language as the global default.
310
// Called from a translation file.
311
fc.lang = function(langCode, options) {
312
	var langOptions;
313
314
	if (options) {
315
		langOptions = langOptionHash[langCode];
316
317
		// initialize the hash for this language
318
		if (!langOptions) {
319
			langOptions = langOptionHash[langCode] = {};
320
		}
321
322
		mergeOptions(langOptions, options || {});
323
	}
324
325
	// set it as the default language for FullCalendar
326
	defaults.lang = langCode;
327
};
328
;;
329
330
 
331
function Calendar(element, instanceOptions) {
332
	var t = this;
333
334
335
336
	// Build options object
337
	// -----------------------------------------------------------------------------------
338
	// Precedence (lowest to highest): defaults, rtlDefaults, langOptions, instanceOptions
339
340
	instanceOptions = instanceOptions || {};
341
342
	var options = mergeOptions({}, defaults, instanceOptions);
343
	var langOptions;
344
345
	// determine language options
346
	if (options.lang in langOptionHash) {
347
		langOptions = langOptionHash[options.lang];
348
	}
349
	else {
350
		langOptions = langOptionHash[defaults.lang];
351
	}
352
353
	if (langOptions) { // if language options exist, rebuild...
354
		options = mergeOptions({}, defaults, langOptions, instanceOptions);
355
	}
356
357
	if (options.isRTL) { // is isRTL, rebuild...
358
		options = mergeOptions({}, defaults, rtlDefaults, langOptions || {}, instanceOptions);
359
	}
360
361
362
	
363
	// Exports
364
	// -----------------------------------------------------------------------------------
365
366
	t.options = options;
367
	t.render = render;
368
	t.destroy = destroy;
369
	t.refetchEvents = refetchEvents;
370
	t.reportEvents = reportEvents;
371
	t.reportEventChange = reportEventChange;
372
	t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method
373
	t.changeView = changeView;
374
	t.select = select;
375
	t.unselect = unselect;
376
	t.prev = prev;
377
	t.next = next;
378
	t.prevYear = prevYear;
379
	t.nextYear = nextYear;
380
	t.today = today;
381
	t.gotoDate = gotoDate;
382
	t.incrementDate = incrementDate;
383
	t.zoomTo = zoomTo;
384
	t.getDate = getDate;
385
	t.getCalendar = getCalendar;
386
	t.getView = getView;
387
	t.option = option;
388
	t.trigger = trigger;
389
390
391
392
	// Language-data Internals
393
	// -----------------------------------------------------------------------------------
394
	// Apply overrides to the current language's data
395
396
397
	// Returns moment's internal locale data. If doesn't exist, returns English.
398
	// Works with moment-pre-2.8
399
	function getLocaleData(langCode) {
400
		var f = moment.localeData || moment.langData;
401
		return f.call(moment, langCode) ||
402
			f.call(moment, 'en'); // the newer localData could return null, so fall back to en
403
	}
404
405
406
	var localeData = createObject(getLocaleData(options.lang)); // make a cheap copy
407
408
	if (options.monthNames) {
409
		localeData._months = options.monthNames;
410
	}
411
	if (options.monthNamesShort) {
412
		localeData._monthsShort = options.monthNamesShort;
413
	}
414
	if (options.dayNames) {
415
		localeData._weekdays = options.dayNames;
416
	}
417
	if (options.dayNamesShort) {
418
		localeData._weekdaysShort = options.dayNamesShort;
419
	}
420
	if (options.firstDay != null) {
421
		var _week = createObject(localeData._week); // _week: { dow: # }
422
		_week.dow = options.firstDay;
423
		localeData._week = _week;
424
	}
425
426
427
428
	// Calendar-specific Date Utilities
429
	// -----------------------------------------------------------------------------------
430
431
432
	t.defaultAllDayEventDuration = moment.duration(options.defaultAllDayEventDuration);
433
	t.defaultTimedEventDuration = moment.duration(options.defaultTimedEventDuration);
434
435
436
	// Builds a moment using the settings of the current calendar: timezone and language.
437
	// Accepts anything the vanilla moment() constructor accepts.
438
	t.moment = function() {
439
		var mom;
440
441
		if (options.timezone === 'local') {
442
			mom = fc.moment.apply(null, arguments);
443
444
			// Force the moment to be local, because fc.moment doesn't guarantee it.
445
			if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone
446
				mom.local();
447
			}
448
		}
449
		else if (options.timezone === 'UTC') {
450
			mom = fc.moment.utc.apply(null, arguments); // process as UTC
451
		}
452
		else {
453
			mom = fc.moment.parseZone.apply(null, arguments); // let the input decide the zone
454
		}
455
456
		if ('_locale' in mom) { // moment 2.8 and above
457
			mom._locale = localeData;
458
		}
459
		else { // pre-moment-2.8
460
			mom._lang = localeData;
461
		}
462
463
		return mom;
464
	};
465
466
467
	// Returns a boolean about whether or not the calendar knows how to calculate
468
	// the timezone offset of arbitrary dates in the current timezone.
469
	t.getIsAmbigTimezone = function() {
470
		return options.timezone !== 'local' && options.timezone !== 'UTC';
471
	};
472
473
474
	// Returns a copy of the given date in the current timezone of it is ambiguously zoned.
475
	// This will also give the date an unambiguous time.
476
	t.rezoneDate = function(date) {
477
		return t.moment(date.toArray());
478
	};
479
480
481
	// Returns a moment for the current date, as defined by the client's computer,
482
	// or overridden by the `now` option.
483
	t.getNow = function() {
484
		var now = options.now;
485
		if (typeof now === 'function') {
486
			now = now();
487
		}
488
		return t.moment(now);
489
	};
490
491
492
	// Calculates the week number for a moment according to the calendar's
493
	// `weekNumberCalculation` setting.
494
	t.calculateWeekNumber = function(mom) {
495
		var calc = options.weekNumberCalculation;
496
497
		if (typeof calc === 'function') {
498
			return calc(mom);
499
		}
500
		else if (calc === 'local') {
501
			return mom.week();
502
		}
503
		else if (calc.toUpperCase() === 'ISO') {
504
			return mom.isoWeek();
505
		}
506
	};
507
508
509
	// Get an event's normalized end date. If not present, calculate it from the defaults.
510
	t.getEventEnd = function(event) {
511
		if (event.end) {
512
			return event.end.clone();
513
		}
514
		else {
515
			return t.getDefaultEventEnd(event.allDay, event.start);
516
		}
517
	};
518
519
520
	// Given an event's allDay status and start date, return swhat its fallback end date should be.
521
	t.getDefaultEventEnd = function(allDay, start) { // TODO: rename to computeDefaultEventEnd
522
		var end = start.clone();
523
524
		if (allDay) {
525
			end.stripTime().add(t.defaultAllDayEventDuration);
526
		}
527
		else {
528
			end.add(t.defaultTimedEventDuration);
529
		}
530
531
		if (t.getIsAmbigTimezone()) {
532
			end.stripZone(); // we don't know what the tzo should be
533
		}
534
535
		return end;
536
	};
537
538
539
540
	// Date-formatting Utilities
541
	// -----------------------------------------------------------------------------------
542
543
544
	// Like the vanilla formatRange, but with calendar-specific settings applied.
545
	t.formatRange = function(m1, m2, formatStr) {
546
547
		// a function that returns a formatStr // TODO: in future, precompute this
548
		if (typeof formatStr === 'function') {
549
			formatStr = formatStr.call(t, options, localeData);
550
		}
551
552
		return formatRange(m1, m2, formatStr, null, options.isRTL);
553
	};
554
555
556
	// Like the vanilla formatDate, but with calendar-specific settings applied.
557
	t.formatDate = function(mom, formatStr) {
558
559
		// a function that returns a formatStr // TODO: in future, precompute this
560
		if (typeof formatStr === 'function') {
561
			formatStr = formatStr.call(t, options, localeData);
562
		}
563
564
		return formatDate(mom, formatStr);
565
	};
566
567
568
	
569
	// Imports
570
	// -----------------------------------------------------------------------------------
571
572
573
	EventManager.call(t, options);
574
	var isFetchNeeded = t.isFetchNeeded;
575
	var fetchEvents = t.fetchEvents;
576
577
578
579
	// Locals
580
	// -----------------------------------------------------------------------------------
581
582
583
	var _element = element[0];
584
	var header;
585
	var headerElement;
586
	var content;
587
	var tm; // for making theme classes
588
	var currentView;
589
	var suggestedViewHeight;
590
	var windowResizeProxy; // wraps the windowResize function
591
	var ignoreWindowResize = 0;
592
	var date;
593
	var events = [];
594
	
595
	
596
	
597
	// Main Rendering
598
	// -----------------------------------------------------------------------------------
599
600
601
	if (options.defaultDate != null) {
602
		date = t.moment(options.defaultDate);
603
	}
604
	else {
605
		date = t.getNow();
606
	}
607
	
608
	
609
	function render(inc) {
610
		if (!content) {
611
			initialRender();
612
		}
613
		else if (elementVisible()) {
614
			// mainly for the public API
615
			calcSize();
616
			renderView(inc);
617
		}
618
	}
619
	
620
	
621
	function initialRender() {
622
		tm = options.theme ? 'ui' : 'fc';
0 ignored issues
show
Unused Code introduced by
The variable tm seems to be never used. Consider removing it.
Loading history...
623
		element.addClass('fc');
624
625
		if (options.isRTL) {
626
			element.addClass('fc-rtl');
627
		}
628
		else {
629
			element.addClass('fc-ltr');
630
		}
631
632
		if (options.theme) {
633
			element.addClass('ui-widget');
634
		}
635
		else {
636
			element.addClass('fc-unthemed');
637
		}
638
639
		content = $("<div class='fc-view-container'/>").prependTo(element);
640
641
		header = new Header(t, options);
642
		headerElement = header.render();
643
		if (headerElement) {
644
			element.prepend(headerElement);
645
		}
646
647
		changeView(options.defaultView);
648
649
		if (options.handleWindowResize) {
650
			windowResizeProxy = debounce(windowResize, options.windowResizeDelay); // prevents rapid calls
651
			$(window).resize(windowResizeProxy);
652
		}
653
	}
654
	
655
	
656
	function destroy() {
657
658
		if (currentView) {
659
			currentView.destroy();
660
		}
661
662
		header.destroy();
663
		content.remove();
664
		element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
665
666
		$(window).unbind('resize', windowResizeProxy);
667
	}
668
	
669
	
670
	function elementVisible() {
671
		return element.is(':visible');
672
	}
673
	
674
	
675
676
	// View Rendering
677
	// -----------------------------------------------------------------------------------
678
679
680
	function changeView(viewName) {
681
		renderView(0, viewName);
682
	}
683
684
685
	// Renders a view because of a date change, view-type change, or for the first time
686
	function renderView(delta, viewName) {
687
		ignoreWindowResize++;
688
689
		// if viewName is changing, destroy the old view
690
		if (currentView && viewName && currentView.name !== viewName) {
691
			header.deactivateButton(currentView.name);
692
			freezeContentHeight(); // prevent a scroll jump when view element is removed
693
			if (currentView.start) { // rendered before?
694
				currentView.destroy();
695
			}
696
			currentView.el.remove();
697
			currentView = null;
698
		}
699
700
		// if viewName changed, or the view was never created, create a fresh view
701
		if (!currentView && viewName) {
702
			currentView = new fcViews[viewName](t);
703
			currentView.el =  $("<div class='fc-view fc-" + viewName + "-view' />").appendTo(content);
704
			header.activateButton(viewName);
705
		}
706
707
		if (currentView) {
708
709
			// let the view determine what the delta means
710
			if (delta) {
711
				date = currentView.incrementDate(date, delta);
712
			}
713
714
			// render or rerender the view
715
			if (
716
				!currentView.start || // never rendered before
717
				delta || // explicit date window change
718
				!date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change
719
			) {
720
				if (elementVisible()) {
721
722
					freezeContentHeight();
723
					if (currentView.start) { // rendered before?
724
						currentView.destroy();
725
					}
726
					currentView.render(date);
727
					unfreezeContentHeight();
728
729
					// need to do this after View::render, so dates are calculated
730
					updateTitle();
731
					updateTodayButton();
732
733
					getAndRenderEvents();
734
				}
735
			}
736
		}
737
738
		unfreezeContentHeight(); // undo any lone freezeContentHeight calls
739
		ignoreWindowResize--;
740
	}
741
	
742
	
743
744
	// Resizing
745
	// -----------------------------------------------------------------------------------
746
747
748
	t.getSuggestedViewHeight = function() {
749
		if (suggestedViewHeight === undefined) {
750
			calcSize();
751
		}
752
		return suggestedViewHeight;
753
	};
754
755
756
	t.isHeightAuto = function() {
757
		return options.contentHeight === 'auto' || options.height === 'auto';
758
	};
759
	
760
	
761
	function updateSize(shouldRecalc) {
762
		if (elementVisible()) {
763
764
			if (shouldRecalc) {
765
				_calcSize();
766
			}
767
768
			ignoreWindowResize++;
769
			currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto()
770
			ignoreWindowResize--;
771
772
			return true; // signal success
773
		}
774
	}
775
776
777
	function calcSize() {
778
		if (elementVisible()) {
779
			_calcSize();
780
		}
781
	}
782
	
783
	
784
	function _calcSize() { // assumes elementVisible
785
		if (typeof options.contentHeight === 'number') { // exists and not 'auto'
786
			suggestedViewHeight = options.contentHeight;
787
		}
788
		else if (typeof options.height === 'number') { // exists and not 'auto'
789
			suggestedViewHeight = options.height - (headerElement ? headerElement.outerHeight(true) : 0);
790
		}
791
		else {
792
			suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));
793
		}
794
	}
795
	
796
	
797
	function windowResize(ev) {
798
		if (
799
			!ignoreWindowResize &&
800
			ev.target === window && // so we don't process jqui "resize" events that have bubbled up
801
			currentView.start // view has already been rendered
802
		) {
803
			if (updateSize(true)) {
804
				currentView.trigger('windowResize', _element);
805
			}
806
		}
807
	}
808
	
809
	
810
	
811
	/* Event Fetching/Rendering
812
	-----------------------------------------------------------------------------*/
813
	// TODO: going forward, most of this stuff should be directly handled by the view
814
815
816
	function refetchEvents() { // can be called as an API method
817
		destroyEvents(); // so that events are cleared before user starts waiting for AJAX
818
		fetchAndRenderEvents();
819
	}
820
821
822
	function renderEvents() { // destroys old events if previously rendered
823
		if (elementVisible()) {
824
			freezeContentHeight();
825
			currentView.destroyEvents(); // no performance cost if never rendered
826
			currentView.renderEvents(events);
827
			unfreezeContentHeight();
828
		}
829
	}
830
831
832
	function destroyEvents() {
833
		freezeContentHeight();
834
		currentView.destroyEvents();
835
		unfreezeContentHeight();
836
	}
837
	
838
839
	function getAndRenderEvents() {
840
		if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) {
841
			fetchAndRenderEvents();
842
		}
843
		else {
844
			renderEvents();
845
		}
846
	}
847
848
849
	function fetchAndRenderEvents() {
850
		fetchEvents(currentView.start, currentView.end);
851
			// ... will call reportEvents
852
			// ... which will call renderEvents
853
	}
854
855
	
856
	// called when event data arrives
857
	function reportEvents(_events) {
858
		events = _events;
859
		renderEvents();
860
	}
861
862
863
	// called when a single event's data has been changed
864
	function reportEventChange() {
865
		renderEvents();
866
	}
867
868
869
870
	/* Header Updating
871
	-----------------------------------------------------------------------------*/
872
873
874
	function updateTitle() {
875
		header.updateTitle(currentView.title);
876
	}
877
878
879
	function updateTodayButton() {
880
		var now = t.getNow();
881
		if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) {
882
			header.disableButton('today');
883
		}
884
		else {
885
			header.enableButton('today');
886
		}
887
	}
888
	
889
890
891
	/* Selection
892
	-----------------------------------------------------------------------------*/
893
	
894
895
	function select(start, end) {
896
897
		start = t.moment(start);
898
		if (end) {
899
			end = t.moment(end);
900
		}
901
		else if (start.hasTime()) {
902
			end = start.clone().add(t.defaultTimedEventDuration);
903
		}
904
		else {
905
			end = start.clone().add(t.defaultAllDayEventDuration);
906
		}
907
908
		currentView.select(start, end);
909
	}
910
	
911
912
	function unselect() { // safe to be called before renderView
913
		if (currentView) {
914
			currentView.unselect();
915
		}
916
	}
917
	
918
	
919
	
920
	/* Date
921
	-----------------------------------------------------------------------------*/
922
	
923
	
924
	function prev() {
925
		renderView(-1);
926
	}
927
	
928
	
929
	function next() {
930
		renderView(1);
931
	}
932
	
933
	
934
	function prevYear() {
935
		date.add(-1, 'years');
936
		renderView();
937
	}
938
	
939
	
940
	function nextYear() {
941
		date.add(1, 'years');
942
		renderView();
943
	}
944
	
945
	
946
	function today() {
947
		date = t.getNow();
948
		renderView();
949
	}
950
	
951
	
952
	function gotoDate(dateInput) {
953
		date = t.moment(dateInput);
954
		renderView();
955
	}
956
	
957
	
958
	function incrementDate(delta) {
959
		date.add(moment.duration(delta));
960
		renderView();
961
	}
962
963
964
	// Forces navigation to a view for the given date.
965
	// `viewName` can be a specific view name or a generic one like "week" or "day".
966
	function zoomTo(newDate, viewName) {
967
		var viewStr;
968
		var match;
969
970
		if (!viewName || fcViews[viewName] === undefined) { // a general view name, or "auto"
971
			viewName = viewName || 'day';
972
			viewStr = header.getViewsWithButtons().join(' '); // space-separated string of all the views in the header
973
974
			// try to match a general view name, like "week", against a specific one, like "agendaWeek"
975
			match = viewStr.match(new RegExp('\\w+' + capitaliseFirstLetter(viewName)));
976
977
			// fall back to the day view being used in the header
978
			if (!match) {
979
				match = viewStr.match(/\w+Day/);
980
			}
981
982
			viewName = match ? match[0] : 'agendaDay'; // fall back to agendaDay
983
		}
984
985
		date = newDate;
986
		changeView(viewName);
987
	}
988
	
989
	
990
	function getDate() {
991
		return date.clone();
992
	}
993
994
995
996
	/* Height "Freezing"
997
	-----------------------------------------------------------------------------*/
998
999
1000
	function freezeContentHeight() {
1001
		content.css({
1002
			width: '100%',
1003
			height: content.height(),
1004
			overflow: 'hidden'
1005
		});
1006
	}
1007
1008
1009
	function unfreezeContentHeight() {
1010
		content.css({
1011
			width: '',
1012
			height: '',
1013
			overflow: ''
1014
		});
1015
	}
1016
	
1017
	
1018
	
1019
	/* Misc
1020
	-----------------------------------------------------------------------------*/
1021
	
1022
1023
	function getCalendar() {
1024
		return t;
1025
	}
1026
1027
	
1028
	function getView() {
1029
		return currentView;
1030
	}
1031
	
1032
	
1033
	function option(name, value) {
1034
		if (value === undefined) {
1035
			return options[name];
1036
		}
1037
		if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {
1038
			options[name] = value;
1039
			updateSize(true); // true = allow recalculation of height
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
1040
		}
1041
	}
1042
	
1043
	
1044
	function trigger(name, thisObj) {
1045
		if (options[name]) {
1046
			return options[name].apply(
1047
				thisObj || _element,
1048
				Array.prototype.slice.call(arguments, 2)
1049
			);
1050
		}
1051
	}
1052
1053
}
1054
1055
;;
1056
1057
/* Top toolbar area with buttons and title
1058
----------------------------------------------------------------------------------------------------------------------*/
1059
// TODO: rename all header-related things to "toolbar"
1060
1061
function Header(calendar, options) {
1062
	var t = this;
1063
	
1064
	// exports
1065
	t.render = render;
1066
	t.destroy = destroy;
1067
	t.updateTitle = updateTitle;
1068
	t.activateButton = activateButton;
1069
	t.deactivateButton = deactivateButton;
1070
	t.disableButton = disableButton;
1071
	t.enableButton = enableButton;
1072
	t.getViewsWithButtons = getViewsWithButtons;
1073
	
1074
	// locals
1075
	var el = $();
1076
	var viewsWithButtons = [];
1077
	var tm;
1078
1079
1080
	function render() {
1081
		var sections = options.header;
1082
1083
		tm = options.theme ? 'ui' : 'fc';
1084
1085
		if (sections) {
1086
			el = $("<div class='fc-toolbar'/>")
1087
				.append(renderSection('left'))
1088
				.append(renderSection('right'))
1089
				.append(renderSection('center'))
1090
				.append('<div class="fc-clear"/>');
1091
1092
			return el;
1093
		}
1094
	}
1095
	
1096
	
1097
	function destroy() {
1098
		el.remove();
1099
	}
1100
	
1101
	
1102
	function renderSection(position) {
1103
		var sectionEl = $('<div class="fc-' + position + '"/>');
1104
		var buttonStr = options.header[position];
1105
1106
		if (buttonStr) {
1107
			$.each(buttonStr.split(' '), function(i) {
1108
				var groupChildren = $();
1109
				var isOnlyButtons = true;
1110
				var groupEl;
1111
1112
				$.each(this.split(','), function(j, buttonName) {
1113
					var buttonClick;
1114
					var themeIcon;
1115
					var normalIcon;
1116
					var defaultText;
1117
					var customText;
1118
					var innerHtml;
1119
					var classes;
1120
					var button;
1121
1122
					if (buttonName == 'title') {
1123
						groupChildren = groupChildren.add($('<h2>&nbsp;</h2>')); // we always want it to take up height
1124
						isOnlyButtons = false;
1125
					}
1126
					else {
1127
						if (calendar[buttonName]) { // a calendar method
1128
							buttonClick = function() {
1129
								calendar[buttonName]();
1130
							};
1131
						}
1132
						else if (fcViews[buttonName]) { // a view name
1133
							buttonClick = function() {
1134
								calendar.changeView(buttonName);
1135
							};
1136
							viewsWithButtons.push(buttonName);
1137
						}
1138
						if (buttonClick) {
1139
1140
							// smartProperty allows different text per view button (ex: "Agenda Week" vs "Basic Week")
1141
							themeIcon = smartProperty(options.themeButtonIcons, buttonName);
1142
							normalIcon = smartProperty(options.buttonIcons, buttonName);
1143
							defaultText = smartProperty(options.defaultButtonText, buttonName);
1144
							customText = smartProperty(options.buttonText, buttonName);
1145
1146
							if (customText) {
1147
								innerHtml = htmlEscape(customText);
1148
							}
1149
							else if (themeIcon && options.theme) {
1150
								innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>";
1151
							}
1152
							else if (normalIcon && !options.theme) {
1153
								innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>";
1154
							}
1155
							else {
1156
								innerHtml = htmlEscape(defaultText || buttonName);
1157
							}
1158
1159
							classes = [
1160
								'fc-' + buttonName + '-button',
1161
								tm + '-button',
1162
								tm + '-state-default'
1163
							];
1164
1165
							button = $( // type="button" so that it doesn't submit a form
1166
								'<button type="button" class="' + classes.join(' ') + '">' +
1167
									innerHtml +
1168
								'</button>'
1169
								)
1170
								.click(function() {
1171
									// don't process clicks for disabled buttons
1172
									if (!button.hasClass(tm + '-state-disabled')) {
1173
1174
										buttonClick();
1175
1176
										// after the click action, if the button becomes the "active" tab, or disabled,
1177
										// it should never have a hover class, so remove it now.
1178
										if (
1179
											button.hasClass(tm + '-state-active') ||
1180
											button.hasClass(tm + '-state-disabled')
1181
										) {
1182
											button.removeClass(tm + '-state-hover');
1183
										}
1184
									}
1185
								})
1186
								.mousedown(function() {
1187
									// the *down* effect (mouse pressed in).
1188
									// only on buttons that are not the "active" tab, or disabled
1189
									button
1190
										.not('.' + tm + '-state-active')
1191
										.not('.' + tm + '-state-disabled')
1192
										.addClass(tm + '-state-down');
1193
								})
1194
								.mouseup(function() {
1195
									// undo the *down* effect
1196
									button.removeClass(tm + '-state-down');
1197
								})
1198
								.hover(
1199
									function() {
1200
										// the *hover* effect.
1201
										// only on buttons that are not the "active" tab, or disabled
1202
										button
1203
											.not('.' + tm + '-state-active')
1204
											.not('.' + tm + '-state-disabled')
1205
											.addClass(tm + '-state-hover');
1206
									},
1207
									function() {
1208
										// undo the *hover* effect
1209
										button
1210
											.removeClass(tm + '-state-hover')
1211
											.removeClass(tm + '-state-down'); // if mouseleave happens before mouseup
1212
									}
1213
								);
1214
1215
							groupChildren = groupChildren.add(button);
1216
						}
1217
					}
1218
				});
1219
1220
				if (isOnlyButtons) {
1221
					groupChildren
1222
						.first().addClass(tm + '-corner-left').end()
1223
						.last().addClass(tm + '-corner-right').end();
1224
				}
1225
1226
				if (groupChildren.length > 1) {
1227
					groupEl = $('<div/>');
1228
					if (isOnlyButtons) {
1229
						groupEl.addClass('fc-button-group');
1230
					}
1231
					groupEl.append(groupChildren);
1232
					sectionEl.append(groupEl);
1233
				}
1234
				else {
1235
					sectionEl.append(groupChildren); // 1 or 0 children
1236
				}
1237
			});
1238
		}
1239
1240
		return sectionEl;
1241
	}
1242
	
1243
	
1244
	function updateTitle(text) {
1245
		el.find('h2').text(text);
1246
	}
1247
	
1248
	
1249
	function activateButton(buttonName) {
1250
		el.find('.fc-' + buttonName + '-button')
1251
			.addClass(tm + '-state-active');
1252
	}
1253
	
1254
	
1255
	function deactivateButton(buttonName) {
1256
		el.find('.fc-' + buttonName + '-button')
1257
			.removeClass(tm + '-state-active');
1258
	}
1259
	
1260
	
1261
	function disableButton(buttonName) {
1262
		el.find('.fc-' + buttonName + '-button')
1263
			.attr('disabled', 'disabled')
1264
			.addClass(tm + '-state-disabled');
1265
	}
1266
	
1267
	
1268
	function enableButton(buttonName) {
1269
		el.find('.fc-' + buttonName + '-button')
1270
			.removeAttr('disabled')
1271
			.removeClass(tm + '-state-disabled');
1272
	}
1273
1274
1275
	function getViewsWithButtons() {
1276
		return viewsWithButtons;
1277
	}
1278
1279
}
1280
1281
;;
1282
1283
fc.sourceNormalizers = [];
1284
fc.sourceFetchers = [];
1285
1286
var ajaxDefaults = {
1287
	dataType: 'json',
1288
	cache: false
1289
};
1290
1291
var eventGUID = 1;
1292
1293
1294
function EventManager(options) { // assumed to be a calendar
1295
	var t = this;
1296
	
1297
	
1298
	// exports
1299
	t.isFetchNeeded = isFetchNeeded;
1300
	t.fetchEvents = fetchEvents;
1301
	t.addEventSource = addEventSource;
1302
	t.removeEventSource = removeEventSource;
1303
	t.updateEvent = updateEvent;
1304
	t.renderEvent = renderEvent;
1305
	t.removeEvents = removeEvents;
1306
	t.clientEvents = clientEvents;
1307
	t.mutateEvent = mutateEvent;
1308
	
1309
	
1310
	// imports
1311
	var trigger = t.trigger;
1312
	var getView = t.getView;
1313
	var reportEvents = t.reportEvents;
1314
	var getEventEnd = t.getEventEnd;
1315
	
1316
	
1317
	// locals
1318
	var stickySource = { events: [] };
1319
	var sources = [ stickySource ];
1320
	var rangeStart, rangeEnd;
1321
	var currentFetchID = 0;
1322
	var pendingSourceCnt = 0;
1323
	var loadingLevel = 0;
1324
	var cache = [];
1325
1326
1327
	$.each(
1328
		(options.events ? [ options.events ] : []).concat(options.eventSources || []),
1329
		function(i, sourceInput) {
1330
			var source = buildEventSource(sourceInput);
1331
			if (source) {
1332
				sources.push(source);
1333
			}
1334
		}
1335
	);
1336
	
1337
	
1338
	
1339
	/* Fetching
1340
	-----------------------------------------------------------------------------*/
1341
	
1342
	
1343
	function isFetchNeeded(start, end) {
1344
		return !rangeStart || // nothing has been fetched yet?
1345
			// or, a part of the new range is outside of the old range? (after normalizing)
1346
			start.clone().stripZone() < rangeStart.clone().stripZone() ||
1347
			end.clone().stripZone() > rangeEnd.clone().stripZone();
1348
	}
1349
	
1350
	
1351
	function fetchEvents(start, end) {
1352
		rangeStart = start;
1353
		rangeEnd = end;
1354
		cache = [];
1355
		var fetchID = ++currentFetchID;
1356
		var len = sources.length;
1357
		pendingSourceCnt = len;
1358
		for (var i=0; i<len; i++) {
1359
			fetchEventSource(sources[i], fetchID);
1360
		}
1361
	}
1362
	
1363
	
1364
	function fetchEventSource(source, fetchID) {
1365
		_fetchEventSource(source, function(events) {
1366
			var isArraySource = $.isArray(source.events);
1367
			var i;
1368
			var event;
1369
1370
			if (fetchID == currentFetchID) {
1371
1372
				if (events) {
1373
					for (i=0; i<events.length; i++) {
1374
						event = events[i];
1375
1376
						// event array sources have already been convert to Event Objects
1377
						if (!isArraySource) {
1378
							event = buildEvent(event, source);
1379
						}
1380
1381
						if (event) {
1382
							cache.push(event);
1383
						}
1384
					}
1385
				}
1386
1387
				pendingSourceCnt--;
1388
				if (!pendingSourceCnt) {
1389
					reportEvents(cache);
1390
				}
1391
			}
1392
		});
1393
	}
1394
	
1395
	
1396
	function _fetchEventSource(source, callback) {
1397
		var i;
1398
		var fetchers = fc.sourceFetchers;
1399
		var res;
1400
1401
		for (i=0; i<fetchers.length; i++) {
1402
			res = fetchers[i].call(
1403
				t, // this, the Calendar object
1404
				source,
1405
				rangeStart.clone(),
1406
				rangeEnd.clone(),
1407
				options.timezone,
1408
				callback
1409
			);
1410
1411
			if (res === true) {
1412
				// the fetcher is in charge. made its own async request
1413
				return;
1414
			}
1415
			else if (typeof res == 'object') {
1416
				// the fetcher returned a new source. process it
1417
				_fetchEventSource(res, callback);
1418
				return;
1419
			}
1420
		}
1421
1422
		var events = source.events;
1423
		if (events) {
1424
			if ($.isFunction(events)) {
1425
				pushLoading();
1426
				events.call(
1427
					t, // this, the Calendar object
1428
					rangeStart.clone(),
1429
					rangeEnd.clone(),
1430
					options.timezone,
1431
					function(events) {
1432
						callback(events);
1433
						popLoading();
1434
					}
1435
				);
1436
			}
1437
			else if ($.isArray(events)) {
1438
				callback(events);
1439
			}
1440
			else {
1441
				callback();
1442
			}
1443
		}else{
1444
			var url = source.url;
1445
			if (url) {
1446
				var success = source.success;
1447
				var error = source.error;
1448
				var complete = source.complete;
1449
1450
				// retrieve any outbound GET/POST $.ajax data from the options
1451
				var customData;
1452
				if ($.isFunction(source.data)) {
1453
					// supplied as a function that returns a key/value object
1454
					customData = source.data();
1455
				}
1456
				else {
1457
					// supplied as a straight key/value object
1458
					customData = source.data;
1459
				}
1460
1461
				// use a copy of the custom data so we can modify the parameters
1462
				// and not affect the passed-in object.
1463
				var data = $.extend({}, customData || {});
1464
1465
				var startParam = firstDefined(source.startParam, options.startParam);
1466
				var endParam = firstDefined(source.endParam, options.endParam);
1467
				var timezoneParam = firstDefined(source.timezoneParam, options.timezoneParam);
1468
1469
				if (startParam) {
1470
					data[startParam] = rangeStart.format();
1471
				}
1472
				if (endParam) {
1473
					data[endParam] = rangeEnd.format();
1474
				}
1475
				if (options.timezone && options.timezone != 'local') {
1476
					data[timezoneParam] = options.timezone;
1477
				}
1478
1479
				pushLoading();
1480
				$.ajax($.extend({}, ajaxDefaults, source, {
1481
					data: data,
1482
					success: function(events) {
1483
						events = events || [];
1484
						var res = applyAll(success, this, arguments);
1485
						if ($.isArray(res)) {
1486
							events = res;
1487
						}
1488
						callback(events);
1489
					},
1490
					error: function() {
1491
						applyAll(error, this, arguments);
1492
						callback();
1493
					},
1494
					complete: function() {
1495
						applyAll(complete, this, arguments);
1496
						popLoading();
1497
					}
1498
				}));
1499
			}else{
1500
				callback();
1501
			}
1502
		}
1503
	}
1504
	
1505
	
1506
	
1507
	/* Sources
1508
	-----------------------------------------------------------------------------*/
1509
	
1510
1511
	function addEventSource(sourceInput) {
1512
		var source = buildEventSource(sourceInput);
1513
		if (source) {
1514
			sources.push(source);
1515
			pendingSourceCnt++;
1516
			fetchEventSource(source, currentFetchID); // will eventually call reportEvents
1517
		}
1518
	}
1519
1520
1521
	function buildEventSource(sourceInput) { // will return undefined if invalid source
1522
		var normalizers = fc.sourceNormalizers;
1523
		var source;
1524
		var i;
1525
1526
		if ($.isFunction(sourceInput) || $.isArray(sourceInput)) {
1527
			source = { events: sourceInput };
1528
		}
1529
		else if (typeof sourceInput === 'string') {
1530
			source = { url: sourceInput };
1531
		}
1532
		else if (typeof sourceInput === 'object') {
1533
			source = $.extend({}, sourceInput); // shallow copy
1534
		}
1535
1536
		if (source) {
1537
1538
			// TODO: repeat code, same code for event classNames
1539
			if (source.className) {
1540
				if (typeof source.className === 'string') {
1541
					source.className = source.className.split(/\s+/);
1542
				}
1543
				// otherwise, assumed to be an array
1544
			}
1545
			else {
1546
				source.className = [];
1547
			}
1548
1549
			// for array sources, we convert to standard Event Objects up front
1550
			if ($.isArray(source.events)) {
1551
				source.origArray = source.events; // for removeEventSource
1552
				source.events = $.map(source.events, function(eventInput) {
1553
					return buildEvent(eventInput, source);
1554
				});
1555
			}
1556
1557
			for (i=0; i<normalizers.length; i++) {
1558
				normalizers[i].call(t, source);
1559
			}
1560
1561
			return source;
1562
		}
1563
	}
1564
1565
1566
	function removeEventSource(source) {
1567
		sources = $.grep(sources, function(src) {
1568
			return !isSourcesEqual(src, source);
1569
		});
1570
		// remove all client events from that source
1571
		cache = $.grep(cache, function(e) {
1572
			return !isSourcesEqual(e.source, source);
1573
		});
1574
		reportEvents(cache);
1575
	}
1576
1577
1578
	function isSourcesEqual(source1, source2) {
1579
		return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
1580
	}
1581
1582
1583
	function getSourcePrimitive(source) {
1584
		return (
1585
			(typeof source === 'object') ? // a normalized event source?
1586
				(source.origArray || source.url || source.events) : // get the primitive
1587
				null
1588
		) ||
1589
		source; // the given argument *is* the primitive
1590
	}
1591
	
1592
	
1593
	
1594
	/* Manipulation
1595
	-----------------------------------------------------------------------------*/
1596
1597
1598
	function updateEvent(event) {
1599
1600
		event.start = t.moment(event.start);
1601
		if (event.end) {
1602
			event.end = t.moment(event.end);
1603
		}
1604
1605
		mutateEvent(event);
1606
		propagateMiscProperties(event);
1607
		reportEvents(cache); // reports event modifications (so we can redraw)
1608
	}
1609
1610
1611
	var miscCopyableProps = [
1612
		'title',
1613
		'url',
1614
		'allDay',
1615
		'className',
1616
		'editable',
1617
		'color',
1618
		'backgroundColor',
1619
		'borderColor',
1620
		'textColor'
1621
	];
1622
1623
	function propagateMiscProperties(event) {
1624
		var i;
1625
		var cachedEvent;
1626
		var j;
1627
		var prop;
1628
1629
		for (i=0; i<cache.length; i++) {
1630
			cachedEvent = cache[i];
1631
			if (cachedEvent._id == event._id && cachedEvent !== event) {
1632
				for (j=0; j<miscCopyableProps.length; j++) {
1633
					prop = miscCopyableProps[j];
1634
					if (event[prop] !== undefined) {
1635
						cachedEvent[prop] = event[prop];
1636
					}
1637
				}
1638
			}
1639
		}
1640
	}
1641
1642
	
1643
	
1644
	function renderEvent(eventData, stick) {
1645
		var event = buildEvent(eventData);
1646
		if (event) {
1647
			if (!event.source) {
1648
				if (stick) {
1649
					stickySource.events.push(event);
1650
					event.source = stickySource;
1651
				}
1652
				cache.push(event);
1653
			}
1654
			reportEvents(cache);
1655
		}
1656
	}
1657
	
1658
	
1659
	function removeEvents(filter) {
1660
		var eventID;
1661
		var i;
1662
1663
		if (filter == null) { // null or undefined. remove all events
1664
			filter = function() { return true; }; // will always match
1665
		}
1666
		else if (!$.isFunction(filter)) { // an event ID
1667
			eventID = filter + '';
1668
			filter = function(event) {
1669
				return event._id == eventID;
1670
			};
1671
		}
1672
1673
		// Purge event(s) from our local cache
1674
		cache = $.grep(cache, filter, true); // inverse=true
1675
1676
		// Remove events from array sources.
1677
		// This works because they have been converted to official Event Objects up front.
1678
		// (and as a result, event._id has been calculated).
1679
		for (i=0; i<sources.length; i++) {
1680
			if ($.isArray(sources[i].events)) {
1681
				sources[i].events = $.grep(sources[i].events, filter, true);
1682
			}
1683
		}
1684
1685
		reportEvents(cache);
1686
	}
1687
	
1688
	
1689
	function clientEvents(filter) {
1690
		if ($.isFunction(filter)) {
1691
			return $.grep(cache, filter);
1692
		}
1693
		else if (filter != null) { // not null, not undefined. an event ID
1694
			filter += '';
1695
			return $.grep(cache, function(e) {
1696
				return e._id == filter;
1697
			});
1698
		}
1699
		return cache; // else, return all
1700
	}
1701
	
1702
	
1703
	
1704
	/* Loading State
1705
	-----------------------------------------------------------------------------*/
1706
	
1707
	
1708
	function pushLoading() {
1709
		if (!(loadingLevel++)) {
1710
			trigger('loading', null, true, getView());
1711
		}
1712
	}
1713
	
1714
	
1715
	function popLoading() {
1716
		if (!(--loadingLevel)) {
1717
			trigger('loading', null, false, getView());
1718
		}
1719
	}
1720
	
1721
	
1722
	
1723
	/* Event Normalization
1724
	-----------------------------------------------------------------------------*/
1725
1726
	function buildEvent(data, source) { // source may be undefined!
1727
		var out = {};
1728
		var start;
1729
		var end;
1730
		var allDay;
1731
		var allDayDefault;
1732
1733
		if (options.eventDataTransform) {
1734
			data = options.eventDataTransform(data);
1735
		}
1736
		if (source && source.eventDataTransform) {
1737
			data = source.eventDataTransform(data);
1738
		}
1739
1740
		start = t.moment(data.start || data.date); // "date" is an alias for "start"
1741
		if (!start.isValid()) {
1742
			return;
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
1743
		}
1744
1745
		end = null;
1746
		if (data.end) {
1747
			end = t.moment(data.end);
1748
			if (!end.isValid()) {
1749
				return;
0 ignored issues
show
Comprehensibility Best Practice introduced by
Are you sure this return statement is not missing an argument? If this is intended, consider adding an explicit undefined like return undefined;.
Loading history...
1750
			}
1751
		}
1752
1753
		allDay = data.allDay;
1754
		if (allDay === undefined) {
1755
			allDayDefault = firstDefined(
1756
				source ? source.allDayDefault : undefined,
1757
				options.allDayDefault
1758
			);
1759
			if (allDayDefault !== undefined) {
1760
				// use the default
1761
				allDay = allDayDefault;
1762
			}
1763
			else {
1764
				// all dates need to have ambig time for the event to be considered allDay
1765
				allDay = !start.hasTime() && (!end || !end.hasTime());
1766
			}
1767
		}
1768
1769
		// normalize the date based on allDay
1770
		if (allDay) {
1771
			// neither date should have a time
1772
			if (start.hasTime()) {
1773
				start.stripTime();
1774
			}
1775
			if (end && end.hasTime()) {
1776
				end.stripTime();
1777
			}
1778
		}
1779
		else {
1780
			// force a time/zone up the dates
1781
			if (!start.hasTime()) {
1782
				start = t.rezoneDate(start);
1783
			}
1784
			if (end && !end.hasTime()) {
1785
				end = t.rezoneDate(end);
1786
			}
1787
		}
1788
1789
		// Copy all properties over to the resulting object.
1790
		// The special-case properties will be copied over afterwards.
1791
		$.extend(out, data);
1792
1793
		if (source) {
1794
			out.source = source;
1795
		}
1796
1797
		out._id = data._id || (data.id === undefined ? '_fc' + eventGUID++ : data.id + '');
1798
1799
		if (data.className) {
1800
			if (typeof data.className == 'string') {
1801
				out.className = data.className.split(/\s+/);
1802
			}
1803
			else { // assumed to be an array
1804
				out.className = data.className;
1805
			}
1806
		}
1807
		else {
1808
			out.className = [];
1809
		}
1810
1811
		out.allDay = allDay;
1812
		out.start = start;
1813
		out.end = end;
1814
1815
		if (options.forceEventDuration && !out.end) {
1816
			out.end = getEventEnd(out);
1817
		}
1818
1819
		backupEventDates(out);
1820
1821
		return out;
1822
	}
1823
1824
1825
1826
	/* Event Modification Math
1827
	-----------------------------------------------------------------------------------------*/
1828
1829
1830
	// Modify the date(s) of an event and make this change propagate to all other events with
1831
	// the same ID (related repeating events).
1832
	//
1833
	// If `newStart`/`newEnd` are not specified, the "new" dates are assumed to be `event.start` and `event.end`.
1834
	// The "old" dates to be compare against are always `event._start` and `event._end` (set by EventManager).
1835
	//
1836
	// Returns an object with delta information and a function to undo all operations.
1837
	//
1838
	function mutateEvent(event, newStart, newEnd) {
1839
		var oldAllDay = event._allDay;
1840
		var oldStart = event._start;
1841
		var oldEnd = event._end;
1842
		var clearEnd = false;
1843
		var newAllDay;
1844
		var dateDelta;
1845
		var durationDelta;
1846
		var undoFunc;
1847
1848
		// if no new dates were passed in, compare against the event's existing dates
1849
		if (!newStart && !newEnd) {
1850
			newStart = event.start;
1851
			newEnd = event.end;
1852
		}
1853
1854
		// NOTE: throughout this function, the initial values of `newStart` and `newEnd` are
1855
		// preserved. These values may be undefined.
1856
1857
		// detect new allDay
1858
		if (event.allDay != oldAllDay) { // if value has changed, use it
1859
			newAllDay = event.allDay;
1860
		}
1861
		else { // otherwise, see if any of the new dates are allDay
1862
			newAllDay = !(newStart || newEnd).hasTime();
1863
		}
1864
1865
		// normalize the new dates based on allDay
1866
		if (newAllDay) {
1867
			if (newStart) {
1868
				newStart = newStart.clone().stripTime();
1869
			}
1870
			if (newEnd) {
1871
				newEnd = newEnd.clone().stripTime();
1872
			}
1873
		}
1874
1875
		// compute dateDelta
1876
		if (newStart) {
1877
			if (newAllDay) {
1878
				dateDelta = dayishDiff(newStart, oldStart.clone().stripTime()); // treat oldStart as allDay
1879
			}
1880
			else {
1881
				dateDelta = dayishDiff(newStart, oldStart);
1882
			}
1883
		}
1884
1885
		if (newAllDay != oldAllDay) {
1886
			// if allDay has changed, always throw away the end
1887
			clearEnd = true;
1888
		}
1889
		else if (newEnd) {
1890
			durationDelta = dayishDiff(
1891
				// new duration
1892
				newEnd || t.getDefaultEventEnd(newAllDay, newStart || oldStart),
1893
				newStart || oldStart
1894
			).subtract(dayishDiff(
1895
				// subtract old duration
1896
				oldEnd || t.getDefaultEventEnd(oldAllDay, oldStart),
1897
				oldStart
1898
			));
1899
		}
1900
1901
		undoFunc = mutateEvents(
1902
			clientEvents(event._id), // get events with this ID
1903
			clearEnd,
1904
			newAllDay,
1905
			dateDelta,
0 ignored issues
show
Bug introduced by
The variable dateDelta does not seem to be initialized in case newStart on line 1876 is false. Are you sure the function mutateEvents handles undefined variables?
Loading history...
1906
			durationDelta
0 ignored issues
show
Bug introduced by
The variable durationDelta seems to not be initialized for all possible execution paths. Are you sure mutateEvents handles undefined variables?
Loading history...
1907
		);
1908
1909
		return {
1910
			dateDelta: dateDelta,
1911
			durationDelta: durationDelta,
1912
			undo: undoFunc
1913
		};
1914
	}
1915
1916
1917
	// Modifies an array of events in the following ways (operations are in order):
1918
	// - clear the event's `end`
1919
	// - convert the event to allDay
1920
	// - add `dateDelta` to the start and end
1921
	// - add `durationDelta` to the event's duration
1922
	//
1923
	// Returns a function that can be called to undo all the operations.
1924
	//
1925
	function mutateEvents(events, clearEnd, forceAllDay, dateDelta, durationDelta) {
1926
		var isAmbigTimezone = t.getIsAmbigTimezone();
1927
		var undoFunctions = [];
1928
1929
		$.each(events, function(i, event) {
1930
			var oldAllDay = event._allDay;
1931
			var oldStart = event._start;
1932
			var oldEnd = event._end;
1933
			var newAllDay = forceAllDay != null ? forceAllDay : oldAllDay;
1934
			var newStart = oldStart.clone();
1935
			var newEnd = (!clearEnd && oldEnd) ? oldEnd.clone() : null;
1936
1937
			// NOTE: this function is responsible for transforming `newStart` and `newEnd`,
1938
			// which were initialized to the OLD values first. `newEnd` may be null.
1939
1940
			// normlize newStart/newEnd to be consistent with newAllDay
1941
			if (newAllDay) {
1942
				newStart.stripTime();
1943
				if (newEnd) {
1944
					newEnd.stripTime();
1945
				}
1946
			}
1947
			else {
1948
				if (!newStart.hasTime()) {
1949
					newStart = t.rezoneDate(newStart);
1950
				}
1951
				if (newEnd && !newEnd.hasTime()) {
1952
					newEnd = t.rezoneDate(newEnd);
1953
				}
1954
			}
1955
1956
			// ensure we have an end date if necessary
1957
			if (!newEnd && (options.forceEventDuration || +durationDelta)) {
1958
				newEnd = t.getDefaultEventEnd(newAllDay, newStart);
1959
			}
1960
1961
			// translate the dates
1962
			newStart.add(dateDelta);
1963
			if (newEnd) {
1964
				newEnd.add(dateDelta).add(durationDelta);
1965
			}
1966
1967
			// if the dates have changed, and we know it is impossible to recompute the
1968
			// timezone offsets, strip the zone.
1969
			if (isAmbigTimezone) {
1970
				if (+dateDelta || +durationDelta) {
1971
					newStart.stripZone();
1972
					if (newEnd) {
1973
						newEnd.stripZone();
1974
					}
1975
				}
1976
			}
1977
1978
			event.allDay = newAllDay;
1979
			event.start = newStart;
1980
			event.end = newEnd;
1981
			backupEventDates(event);
1982
1983
			undoFunctions.push(function() {
1984
				event.allDay = oldAllDay;
1985
				event.start = oldStart;
1986
				event.end = oldEnd;
1987
				backupEventDates(event);
1988
			});
1989
		});
1990
1991
		return function() {
1992
			for (var i=0; i<undoFunctions.length; i++) {
1993
				undoFunctions[i]();
1994
			}
1995
		};
1996
	}
1997
1998
}
1999
2000
2001
// updates the "backup" properties, which are preserved in order to compute diffs later on.
2002
function backupEventDates(event) {
2003
	event._allDay = event.allDay;
2004
	event._start = event.start.clone();
2005
	event._end = event.end ? event.end.clone() : null;
2006
}
2007
2008
;;
2009
2010
/* FullCalendar-specific DOM Utilities
2011
----------------------------------------------------------------------------------------------------------------------*/
2012
2013
2014
// Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
2015
// and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
2016
function compensateScroll(rowEls, scrollbarWidths) {
2017
	if (scrollbarWidths.left) {
2018
		rowEls.css({
2019
			'border-left-width': 1,
2020
			'margin-left': scrollbarWidths.left - 1
2021
		});
2022
	}
2023
	if (scrollbarWidths.right) {
2024
		rowEls.css({
2025
			'border-right-width': 1,
2026
			'margin-right': scrollbarWidths.right - 1
2027
		});
2028
	}
2029
}
2030
2031
2032
// Undoes compensateScroll and restores all borders/margins
2033
function uncompensateScroll(rowEls) {
2034
	rowEls.css({
2035
		'margin-left': '',
2036
		'margin-right': '',
2037
		'border-left-width': '',
2038
		'border-right-width': ''
2039
	});
2040
}
2041
2042
2043
// Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
2044
// By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
2045
// any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and 
2046
// reduces the available height.
2047
function distributeHeight(els, availableHeight, shouldRedistribute) {
2048
2049
	// *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
2050
	// and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.
2051
2052
	var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element
2053
	var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE*
2054
	var flexEls = []; // elements that are allowed to expand. array of DOM nodes
2055
	var flexOffsets = []; // amount of vertical space it takes up
2056
	var flexHeights = []; // actual css height
2057
	var usedHeight = 0;
2058
2059
	undistributeHeight(els); // give all elements their natural height
2060
2061
	// find elements that are below the recommended height (expandable).
2062
	// important to query for heights in a single first pass (to avoid reflow oscillation).
2063
	els.each(function(i, el) {
2064
		var minOffset = i === els.length - 1 ? minOffset2 : minOffset1;
2065
		var naturalOffset = $(el).outerHeight(true);
2066
2067
		if (naturalOffset < minOffset) {
2068
			flexEls.push(el);
2069
			flexOffsets.push(naturalOffset);
2070
			flexHeights.push($(el).height());
2071
		}
2072
		else {
2073
			// this element stretches past recommended height (non-expandable). mark the space as occupied.
2074
			usedHeight += naturalOffset;
2075
		}
2076
	});
2077
2078
	// readjust the recommended height to only consider the height available to non-maxed-out rows.
2079
	if (shouldRedistribute) {
2080
		availableHeight -= usedHeight;
2081
		minOffset1 = Math.floor(availableHeight / flexEls.length);
2082
		minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE*
2083
	}
2084
2085
	// assign heights to all expandable elements
2086
	$(flexEls).each(function(i, el) {
2087
		var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1;
2088
		var naturalOffset = flexOffsets[i];
2089
		var naturalHeight = flexHeights[i];
2090
		var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding
2091
2092
		if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things
2093
			$(el).height(newHeight);
2094
		}
2095
	});
2096
}
2097
2098
2099
// Undoes distrubuteHeight, restoring all els to their natural height
2100
function undistributeHeight(els) {
2101
	els.height('');
2102
}
2103
2104
2105
// Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the
2106
// cells to be that width.
2107
// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline
2108
function matchCellWidths(els) {
2109
	var maxInnerWidth = 0;
2110
2111
	els.find('> *').each(function(i, innerEl) {
2112
		var innerWidth = $(innerEl).outerWidth();
2113
		if (innerWidth > maxInnerWidth) {
2114
			maxInnerWidth = innerWidth;
2115
		}
2116
	});
2117
2118
	maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance
2119
2120
	els.width(maxInnerWidth);
2121
2122
	return maxInnerWidth;
2123
}
2124
2125
2126
// Turns a container element into a scroller if its contents is taller than the allotted height.
2127
// Returns true if the element is now a scroller, false otherwise.
2128
// NOTE: this method is best because it takes weird zooming dimensions into account
2129
function setPotentialScroller(containerEl, height) {
2130
	containerEl.height(height).addClass('fc-scroller');
2131
2132
	// are scrollbars needed?
2133
	if (containerEl[0].scrollHeight - 1 > containerEl[0].clientHeight) { // !!! -1 because IE is often off-by-one :(
2134
		return true;
2135
	}
2136
2137
	unsetScroller(containerEl); // undo
2138
	return false;
2139
}
2140
2141
2142
// Takes an element that might have been a scroller, and turns it back into a normal element.
2143
function unsetScroller(containerEl) {
2144
	containerEl.height('').removeClass('fc-scroller');
2145
}
2146
2147
2148
/* General DOM Utilities
2149
----------------------------------------------------------------------------------------------------------------------*/
2150
2151
2152
// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51
2153
function getScrollParent(el) {
2154
	var position = el.css('position'),
2155
		scrollParent = el.parents().filter(function() {
2156
			var parent = $(this);
2157
			return (/(auto|scroll)/).test(
2158
				parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x')
2159
			);
2160
		}).eq(0);
2161
2162
	return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent;
2163
}
2164
2165
2166
// Given a container element, return an object with the pixel values of the left/right scrollbars.
2167
// Left scrollbars might occur on RTL browsers (IE maybe?) but I have not tested.
2168
// PREREQUISITE: container element must have a single child with display:block
2169
function getScrollbarWidths(container) {
2170
	var containerLeft = container.offset().left;
2171
	var containerRight = containerLeft + container.width();
2172
	var inner = container.children();
2173
	var innerLeft = inner.offset().left;
2174
	var innerRight = innerLeft + inner.outerWidth();
2175
2176
	return {
2177
		left: innerLeft - containerLeft,
2178
		right: containerRight - innerRight
2179
	};
2180
}
2181
2182
2183
// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
2184
function isPrimaryMouseButton(ev) {
2185
	return ev.which == 1 && !ev.ctrlKey;
2186
}
2187
2188
2189
/* FullCalendar-specific Misc Utilities
2190
----------------------------------------------------------------------------------------------------------------------*/
2191
2192
2193
// Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection.
2194
// Expects all dates to be normalized to the same timezone beforehand.
2195
function intersectionToSeg(subjectStart, subjectEnd, intervalStart, intervalEnd) {
2196
	var segStart, segEnd;
2197
	var isStart, isEnd;
2198
2199
	if (subjectEnd > intervalStart && subjectStart < intervalEnd) { // in bounds at all?
2200
2201
		if (subjectStart >= intervalStart) {
2202
			segStart = subjectStart.clone();
2203
			isStart = true;
2204
		}
2205
		else {
2206
			segStart = intervalStart.clone();
2207
			isStart =  false;
2208
		}
2209
2210
		if (subjectEnd <= intervalEnd) {
2211
			segEnd = subjectEnd.clone();
2212
			isEnd = true;
2213
		}
2214
		else {
2215
			segEnd = intervalEnd.clone();
2216
			isEnd = false;
2217
		}
2218
2219
		return {
2220
			start: segStart,
2221
			end: segEnd,
2222
			isStart: isStart,
2223
			isEnd: isEnd
2224
		};
2225
	}
2226
}
2227
2228
2229
function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object
2230
	obj = obj || {};
2231
	if (obj[name] !== undefined) {
2232
		return obj[name];
2233
	}
2234
	var parts = name.split(/(?=[A-Z])/),
2235
		i = parts.length - 1, res;
2236
	for (; i>=0; i--) {
2237
		res = obj[parts[i].toLowerCase()];
2238
		if (res !== undefined) {
2239
			return res;
2240
		}
2241
	}
2242
	return obj['default'];
2243
}
2244
2245
2246
/* Date Utilities
2247
----------------------------------------------------------------------------------------------------------------------*/
2248
2249
var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
2250
2251
2252
// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
2253
// Moments will have their timezones normalized.
2254
function dayishDiff(a, b) {
2255
	return moment.duration({
2256
		days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'),
2257
		ms: a.time() - b.time()
2258
	});
2259
}
2260
2261
2262
function isNativeDate(input) {
2263
	return  Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date;
2264
}
2265
2266
2267
function dateCompare(a, b) { // works with Moments and native Dates
2268
	return a - b;
2269
}
2270
2271
2272
/* General Utilities
2273
----------------------------------------------------------------------------------------------------------------------*/
2274
2275
fc.applyAll = applyAll; // export
2276
2277
2278
// Create an object that has the given prototype. Just like Object.create
2279
function createObject(proto) {
2280
	var f = function() {};
2281
	f.prototype = proto;
2282
	return new f();
0 ignored issues
show
Coding Style Best Practice introduced by
By convention, constructors like f should be capitalized.
Loading history...
2283
}
2284
2285
2286
// Copies specifically-owned (non-protoype) properties of `b` onto `a`.
2287
// FYI, $.extend would copy *all* properties of `b` onto `a`.
2288
function extend(a, b) {
2289
	for (var i in b) {
2290
		if (b.hasOwnProperty(i)) {
2291
			a[i] = b[i];
2292
		}
2293
	}
2294
}
2295
2296
2297
function applyAll(functions, thisObj, args) {
2298
	if ($.isFunction(functions)) {
2299
		functions = [ functions ];
2300
	}
2301
	if (functions) {
2302
		var i;
2303
		var ret;
2304
		for (i=0; i<functions.length; i++) {
2305
			ret = functions[i].apply(thisObj, args) || ret;
2306
		}
2307
		return ret;
0 ignored issues
show
Bug introduced by
The variable ret seems to not be initialized for all possible execution paths.
Loading history...
2308
	}
2309
}
2310
2311
2312
function firstDefined() {
2313
	for (var i=0; i<arguments.length; i++) {
2314
		if (arguments[i] !== undefined) {
2315
			return arguments[i];
2316
		}
2317
	}
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
2318
}
2319
2320
2321
function htmlEscape(s) {
2322
	return (s + '').replace(/&/g, '&amp;')
2323
		.replace(/</g, '&lt;')
2324
		.replace(/>/g, '&gt;')
2325
		.replace(/'/g, '&#039;')
2326
		.replace(/"/g, '&quot;')
2327
		.replace(/\n/g, '<br />');
2328
}
2329
2330
2331
function stripHtmlEntities(text) {
2332
	return text.replace(/&.*?;/g, '');
2333
}
2334
2335
2336
function capitaliseFirstLetter(str) {
2337
	return str.charAt(0).toUpperCase() + str.slice(1);
2338
}
2339
2340
2341
// Returns a function, that, as long as it continues to be invoked, will not
2342
// be triggered. The function will be called after it stops being called for
2343
// N milliseconds.
2344
// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
2345
function debounce(func, wait) {
2346
	var timeoutId;
2347
	var args;
2348
	var context;
2349
	var timestamp; // of most recent call
2350
	var later = function() {
2351
		var last = +new Date() - timestamp;
2352
		if (last < wait && last > 0) {
2353
			timeoutId = setTimeout(later, wait - last);
2354
		}
2355
		else {
2356
			timeoutId = null;
2357
			func.apply(context, args);
2358
			if (!timeoutId) {
2359
				context = args = null;
2360
			}
2361
		}
2362
	};
2363
2364
	return function() {
2365
		context = this;
2366
		args = arguments;
2367
		timestamp = +new Date();
2368
		if (!timeoutId) {
2369
			timeoutId = setTimeout(later, wait);
2370
		}
2371
	};
2372
}
2373
2374
;;
2375
2376
var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
2377
var ambigTimeOrZoneRegex =
2378
	/^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/;
2379
2380
2381
// Creating
2382
// -------------------------------------------------------------------------------------------------
2383
2384
// Creates a new moment, similar to the vanilla moment(...) constructor, but with
2385
// extra features (ambiguous time, enhanced formatting). When gived an existing moment,
2386
// it will function as a clone (and retain the zone of the moment). Anything else will
2387
// result in a moment in the local zone.
2388
fc.moment = function() {
2389
	return makeMoment(arguments);
2390
};
2391
2392
// Sames as fc.moment, but forces the resulting moment to be in the UTC timezone.
2393
fc.moment.utc = function() {
2394
	var mom = makeMoment(arguments, true);
2395
2396
	// Force it into UTC because makeMoment doesn't guarantee it.
2397
	if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone
2398
		mom.utc();
2399
	}
2400
2401
	return mom;
2402
};
2403
2404
// Same as fc.moment, but when given an ISO8601 string, the timezone offset is preserved.
2405
// ISO8601 strings with no timezone offset will become ambiguously zoned.
2406
fc.moment.parseZone = function() {
2407
	return makeMoment(arguments, true, true);
2408
};
2409
2410
// Builds an FCMoment from args. When given an existing moment, it clones. When given a native
2411
// Date, or called with no arguments (the current time), the resulting moment will be local.
2412
// Anything else needs to be "parsed" (a string or an array), and will be affected by:
2413
//    parseAsUTC - if there is no zone information, should we parse the input in UTC?
2414
//    parseZone - if there is zone information, should we force the zone of the moment?
2415
function makeMoment(args, parseAsUTC, parseZone) {
2416
	var input = args[0];
2417
	var isSingleString = args.length == 1 && typeof input === 'string';
2418
	var isAmbigTime;
2419
	var isAmbigZone;
2420
	var ambigMatch;
2421
	var output; // an object with fields for the new FCMoment object
2422
2423
	if (moment.isMoment(input)) {
2424
		output = moment.apply(null, args); // clone it
2425
2426
		// the ambig properties have not been preserved in the clone, so reassign them
2427
		if (input._ambigTime) {
2428
			output._ambigTime = true;
2429
		}
2430
		if (input._ambigZone) {
2431
			output._ambigZone = true;
2432
		}
2433
	}
2434
	else if (isNativeDate(input) || input === undefined) {
2435
		output = moment.apply(null, args); // will be local
2436
	}
2437
	else { // "parsing" is required
2438
		isAmbigTime = false;
2439
		isAmbigZone = false;
2440
2441
		if (isSingleString) {
2442
			if (ambigDateOfMonthRegex.test(input)) {
2443
				// accept strings like '2014-05', but convert to the first of the month
2444
				input += '-01';
2445
				args = [ input ]; // for when we pass it on to moment's constructor
2446
				isAmbigTime = true;
2447
				isAmbigZone = true;
2448
			}
2449
			else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) {
2450
				isAmbigTime = !ambigMatch[5]; // no time part?
2451
				isAmbigZone = true;
2452
			}
2453
		}
2454
		else if ($.isArray(input)) {
2455
			// arrays have no timezone information, so assume ambiguous zone
2456
			isAmbigZone = true;
2457
		}
2458
		// otherwise, probably a string with a format
2459
2460
		if (parseAsUTC) {
2461
			output = moment.utc.apply(moment, args);
2462
		}
2463
		else {
2464
			output = moment.apply(null, args);
2465
		}
2466
2467
		if (isAmbigTime) {
2468
			output._ambigTime = true;
2469
			output._ambigZone = true; // ambiguous time always means ambiguous zone
2470
		}
2471
		else if (parseZone) { // let's record the inputted zone somehow
2472
			if (isAmbigZone) {
2473
				output._ambigZone = true;
2474
			}
2475
			else if (isSingleString) {
2476
				output.zone(input); // if not a valid zone, will assign UTC
2477
			}
2478
		}
2479
	}
2480
2481
	return new FCMoment(output);
2482
}
2483
2484
// Our subclass of Moment.
2485
// Accepts an object with the internal Moment properties that should be copied over to
2486
// `this` object (most likely another Moment object). The values in this data must not
2487
// be referenced by anything else (two moments sharing a Date object for example).
2488
function FCMoment(internalData) {
2489
	extend(this, internalData);
2490
}
2491
2492
// Chain the prototype to Moment's
2493
FCMoment.prototype = createObject(moment.fn);
2494
2495
// We need this because Moment's implementation won't create an FCMoment,
2496
// nor will it copy over the ambig flags.
2497
FCMoment.prototype.clone = function() {
2498
	return makeMoment([ this ]);
2499
};
2500
2501
2502
// Time-of-day
2503
// -------------------------------------------------------------------------------------------------
2504
2505
// GETTER
2506
// Returns a Duration with the hours/minutes/seconds/ms values of the moment.
2507
// If the moment has an ambiguous time, a duration of 00:00 will be returned.
2508
//
2509
// SETTER
2510
// You can supply a Duration, a Moment, or a Duration-like argument.
2511
// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous.
2512
FCMoment.prototype.time = function(time) {
2513
	if (time == null) { // getter
2514
		return moment.duration({
2515
			hours: this.hours(),
2516
			minutes: this.minutes(),
2517
			seconds: this.seconds(),
2518
			milliseconds: this.milliseconds()
2519
		});
2520
	}
2521
	else { // setter
2522
2523
		delete this._ambigTime; // mark that the moment now has a time
2524
2525
		if (!moment.isDuration(time) && !moment.isMoment(time)) {
2526
			time = moment.duration(time);
2527
		}
2528
2529
		// The day value should cause overflow (so 24 hours becomes 00:00:00 of next day).
2530
		// Only for Duration times, not Moment times.
2531
		var dayHours = 0;
2532
		if (moment.isDuration(time)) {
2533
			dayHours = Math.floor(time.asDays()) * 24;
2534
		}
2535
2536
		// We need to set the individual fields.
2537
		// Can't use startOf('day') then add duration. In case of DST at start of day.
2538
		return this.hours(dayHours + time.hours())
2539
			.minutes(time.minutes())
2540
			.seconds(time.seconds())
2541
			.milliseconds(time.milliseconds());
2542
	}
2543
};
2544
2545
// Converts the moment to UTC, stripping out its time-of-day and timezone offset,
2546
// but preserving its YMD. A moment with a stripped time will display no time
2547
// nor timezone offset when .format() is called.
2548
FCMoment.prototype.stripTime = function() {
2549
	var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array
2550
2551
	// set the internal UTC flag
2552
	moment.fn.utc.call(this); // call the original method, because we don't want to affect _ambigZone
2553
2554
	this.year(a[0]) // TODO: find a way to do this in one shot
2555
		.month(a[1])
2556
		.date(a[2])
2557
		.hours(0)
2558
		.minutes(0)
2559
		.seconds(0)
2560
		.milliseconds(0);
2561
2562
	// Mark the time as ambiguous. This needs to happen after the .utc() call, which calls .zone(), which
2563
	// clears all ambig flags. Same concept with the .year/month/date calls in the case of moment-timezone.
2564
	this._ambigTime = true;
2565
	this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
2566
2567
	return this; // for chaining
2568
};
2569
2570
// Returns if the moment has a non-ambiguous time (boolean)
2571
FCMoment.prototype.hasTime = function() {
2572
	return !this._ambigTime;
2573
};
2574
2575
2576
// Timezone
2577
// -------------------------------------------------------------------------------------------------
2578
2579
// Converts the moment to UTC, stripping out its timezone offset, but preserving its
2580
// YMD and time-of-day. A moment with a stripped timezone offset will display no
2581
// timezone offset when .format() is called.
2582
FCMoment.prototype.stripZone = function() {
2583
	var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array
2584
	var wasAmbigTime = this._ambigTime;
2585
2586
	moment.fn.utc.call(this); // set the internal UTC flag
2587
2588
	this.year(a[0]) // TODO: find a way to do this in one shot
2589
		.month(a[1])
2590
		.date(a[2])
2591
		.hours(a[3])
2592
		.minutes(a[4])
2593
		.seconds(a[5])
2594
		.milliseconds(a[6]);
2595
2596
	if (wasAmbigTime) {
2597
		// the above call to .utc()/.zone() unfortunately clears the ambig flags, so reassign
2598
		this._ambigTime = true;
2599
	}
2600
2601
	// Mark the zone as ambiguous. This needs to happen after the .utc() call, which calls .zone(), which
2602
	// clears all ambig flags. Same concept with the .year/month/date calls in the case of moment-timezone.
2603
	this._ambigZone = true;
2604
2605
	return this; // for chaining
2606
};
2607
2608
// Returns of the moment has a non-ambiguous timezone offset (boolean)
2609
FCMoment.prototype.hasZone = function() {
2610
	return !this._ambigZone;
2611
};
2612
2613
// this method implicitly marks a zone
2614
FCMoment.prototype.zone = function(tzo) {
2615
2616
	if (tzo != null) {
2617
		// FYI, the delete statements need to be before the .zone() call or else chaos ensues
2618
		// for reasons I don't understand. 
2619
		delete this._ambigTime;
2620
		delete this._ambigZone;
2621
	}
2622
2623
	return moment.fn.zone.apply(this, arguments);
2624
};
2625
2626
// this method implicitly marks a zone
2627
FCMoment.prototype.local = function() {
2628
	var a = this.toArray(); // year,month,date,hours,minutes,seconds as an array
2629
	var wasAmbigZone = this._ambigZone;
2630
2631
	// will happen anyway via .local()/.zone(), but don't want to rely on internal implementation
2632
	delete this._ambigTime;
2633
	delete this._ambigZone;
2634
2635
	moment.fn.local.apply(this, arguments);
2636
2637
	if (wasAmbigZone) {
2638
		// If the moment was ambiguously zoned, the date fields were stored as UTC.
2639
		// We want to preserve these, but in local time.
2640
		this.year(a[0]) // TODO: find a way to do this in one shot
2641
			.month(a[1])
2642
			.date(a[2])
2643
			.hours(a[3])
2644
			.minutes(a[4])
2645
			.seconds(a[5])
2646
			.milliseconds(a[6]);
2647
	}
2648
2649
	return this; // for chaining
2650
};
2651
2652
// this method implicitly marks a zone
2653
FCMoment.prototype.utc = function() {
2654
2655
	// will happen anyway via .local()/.zone(), but don't want to rely on internal implementation
2656
	delete this._ambigTime;
2657
	delete this._ambigZone;
2658
2659
	return moment.fn.utc.apply(this, arguments);
2660
};
2661
2662
2663
// Formatting
2664
// -------------------------------------------------------------------------------------------------
2665
2666
FCMoment.prototype.format = function() {
2667
	if (arguments[0]) {
2668
		return formatDate(this, arguments[0]); // our extended formatting
2669
	}
2670
	if (this._ambigTime) {
2671
		return momentFormat(this, 'YYYY-MM-DD');
2672
	}
2673
	if (this._ambigZone) {
2674
		return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
2675
	}
2676
	return momentFormat(this); // default moment original formatting
2677
};
2678
2679
FCMoment.prototype.toISOString = function() {
2680
	if (this._ambigTime) {
2681
		return momentFormat(this, 'YYYY-MM-DD');
2682
	}
2683
	if (this._ambigZone) {
2684
		return momentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
2685
	}
2686
	return moment.fn.toISOString.apply(this, arguments);
2687
};
2688
2689
2690
// Querying
2691
// -------------------------------------------------------------------------------------------------
2692
2693
// Is the moment within the specified range? `end` is exclusive.
2694
FCMoment.prototype.isWithin = function(start, end) {
2695
	var a = commonlyAmbiguate([ this, start, end ]);
2696
	return a[0] >= a[1] && a[0] < a[2];
2697
};
2698
2699
// When isSame is called with units, timezone ambiguity is normalized before the comparison happens.
2700
// If no units are specified, the two moments must be identically the same, with matching ambig flags.
2701
FCMoment.prototype.isSame = function(input, units) {
2702
	var a;
2703
2704
	if (units) {
2705
		a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times
2706
		return moment.fn.isSame.call(a[0], a[1], units);
2707
	}
2708
	else {
2709
		input = fc.moment.parseZone(input); // normalize input
2710
		return moment.fn.isSame.call(this, input) &&
2711
			Boolean(this._ambigTime) === Boolean(input._ambigTime) &&
2712
			Boolean(this._ambigZone) === Boolean(input._ambigZone);
2713
	}
2714
};
2715
2716
// Make these query methods work with ambiguous moments
2717
$.each([
2718
	'isBefore',
2719
	'isAfter'
2720
], function(i, methodName) {
2721
	FCMoment.prototype[methodName] = function(input, units) {
2722
		var a = commonlyAmbiguate([ this, input ]);
2723
		return moment.fn[methodName].call(a[0], a[1], units);
2724
	};
2725
});
2726
2727
2728
// Misc Internals
2729
// -------------------------------------------------------------------------------------------------
2730
2731
// given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated.
2732
// for example, of one moment has ambig time, but not others, all moments will have their time stripped.
2733
// set `preserveTime` to `true` to keep times, but only normalize zone ambiguity.
2734
function commonlyAmbiguate(inputs, preserveTime) {
2735
	var outputs = [];
2736
	var anyAmbigTime = false;
2737
	var anyAmbigZone = false;
2738
	var i;
2739
2740
	for (i=0; i<inputs.length; i++) {
2741
		outputs.push(fc.moment.parseZone(inputs[i]));
2742
		anyAmbigTime = anyAmbigTime || outputs[i]._ambigTime;
2743
		anyAmbigZone = anyAmbigZone || outputs[i]._ambigZone;
2744
	}
2745
2746
	for (i=0; i<outputs.length; i++) {
2747
		if (anyAmbigTime && !preserveTime) {
2748
			outputs[i].stripTime();
2749
		}
2750
		else if (anyAmbigZone) {
2751
			outputs[i].stripZone();
2752
		}
2753
	}
2754
2755
	return outputs;
2756
}
2757
2758
;;
2759
2760
// Single Date Formatting
2761
// -------------------------------------------------------------------------------------------------
2762
2763
2764
// call this if you want Moment's original format method to be used
2765
function momentFormat(mom, formatStr) {
2766
	return moment.fn.format.call(mom, formatStr);
2767
}
2768
2769
2770
// Formats `date` with a Moment formatting string, but allow our non-zero areas and
2771
// additional token.
2772
function formatDate(date, formatStr) {
2773
	return formatDateWithChunks(date, getFormatStringChunks(formatStr));
2774
}
2775
2776
2777
function formatDateWithChunks(date, chunks) {
2778
	var s = '';
2779
	var i;
2780
2781
	for (i=0; i<chunks.length; i++) {
2782
		s += formatDateWithChunk(date, chunks[i]);
2783
	}
2784
2785
	return s;
2786
}
2787
2788
2789
// addition formatting tokens we want recognized
2790
var tokenOverrides = {
2791
	t: function(date) { // "a" or "p"
2792
		return momentFormat(date, 'a').charAt(0);
2793
	},
2794
	T: function(date) { // "A" or "P"
2795
		return momentFormat(date, 'A').charAt(0);
2796
	}
2797
};
2798
2799
2800
function formatDateWithChunk(date, chunk) {
2801
	var token;
2802
	var maybeStr;
2803
2804
	if (typeof chunk === 'string') { // a literal string
2805
		return chunk;
2806
	}
2807
	else if ((token = chunk.token)) { // a token, like "YYYY"
2808
		if (tokenOverrides[token]) {
2809
			return tokenOverrides[token](date); // use our custom token
2810
		}
2811
		return momentFormat(date, token);
2812
	}
2813
	else if (chunk.maybe) { // a grouping of other chunks that must be non-zero
2814
		maybeStr = formatDateWithChunks(date, chunk.maybe);
2815
		if (maybeStr.match(/[1-9]/)) {
2816
			return maybeStr;
2817
		}
2818
	}
2819
2820
	return '';
2821
}
2822
2823
2824
// Date Range Formatting
2825
// -------------------------------------------------------------------------------------------------
2826
// TODO: make it work with timezone offset
2827
2828
// Using a formatting string meant for a single date, generate a range string, like
2829
// "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
2830
// If the dates are the same as far as the format string is concerned, just return a single
2831
// rendering of one date, without any separator.
2832
function formatRange(date1, date2, formatStr, separator, isRTL) {
2833
	var localeData;
2834
2835
	date1 = fc.moment.parseZone(date1);
2836
	date2 = fc.moment.parseZone(date2);
2837
2838
	localeData = (date1.localeData || date1.lang).call(date1); // works with moment-pre-2.8
2839
2840
	// Expand localized format strings, like "LL" -> "MMMM D YYYY"
2841
	formatStr = localeData.longDateFormat(formatStr) || formatStr;
2842
	// BTW, this is not important for `formatDate` because it is impossible to put custom tokens
2843
	// or non-zero areas in Moment's localized format strings.
2844
2845
	separator = separator || ' - ';
2846
2847
	return formatRangeWithChunks(
2848
		date1,
2849
		date2,
2850
		getFormatStringChunks(formatStr),
2851
		separator,
2852
		isRTL
2853
	);
2854
}
2855
fc.formatRange = formatRange; // expose
2856
2857
2858
function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) {
2859
	var chunkStr; // the rendering of the chunk
2860
	var leftI;
2861
	var leftStr = '';
2862
	var rightI;
2863
	var rightStr = '';
2864
	var middleI;
2865
	var middleStr1 = '';
2866
	var middleStr2 = '';
2867
	var middleStr = '';
2868
2869
	// Start at the leftmost side of the formatting string and continue until you hit a token
2870
	// that is not the same between dates.
2871
	for (leftI=0; leftI<chunks.length; leftI++) {
2872
		chunkStr = formatSimilarChunk(date1, date2, chunks[leftI]);
2873
		if (chunkStr === false) {
2874
			break;
2875
		}
2876
		leftStr += chunkStr;
2877
	}
2878
2879
	// Similarly, start at the rightmost side of the formatting string and move left
2880
	for (rightI=chunks.length-1; rightI>leftI; rightI--) {
2881
		chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]);
2882
		if (chunkStr === false) {
2883
			break;
2884
		}
2885
		rightStr = chunkStr + rightStr;
2886
	}
2887
2888
	// The area in the middle is different for both of the dates.
2889
	// Collect them distinctly so we can jam them together later.
2890
	for (middleI=leftI; middleI<=rightI; middleI++) {
2891
		middleStr1 += formatDateWithChunk(date1, chunks[middleI]);
2892
		middleStr2 += formatDateWithChunk(date2, chunks[middleI]);
2893
	}
2894
2895
	if (middleStr1 || middleStr2) {
2896
		if (isRTL) {
2897
			middleStr = middleStr2 + separator + middleStr1;
2898
		}
2899
		else {
2900
			middleStr = middleStr1 + separator + middleStr2;
2901
		}
2902
	}
2903
2904
	return leftStr + middleStr + rightStr;
2905
}
2906
2907
2908
var similarUnitMap = {
2909
	Y: 'year',
2910
	M: 'month',
2911
	D: 'day', // day of month
2912
	d: 'day', // day of week
2913
	// prevents a separator between anything time-related...
2914
	A: 'second', // AM/PM
2915
	a: 'second', // am/pm
2916
	T: 'second', // A/P
2917
	t: 'second', // a/p
2918
	H: 'second', // hour (24)
2919
	h: 'second', // hour (12)
2920
	m: 'second', // minute
2921
	s: 'second' // second
2922
};
2923
// TODO: week maybe?
2924
2925
2926
// Given a formatting chunk, and given that both dates are similar in the regard the
2927
// formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`.
2928
function formatSimilarChunk(date1, date2, chunk) {
2929
	var token;
2930
	var unit;
2931
2932
	if (typeof chunk === 'string') { // a literal string
2933
		return chunk;
2934
	}
2935
	else if ((token = chunk.token)) {
2936
		unit = similarUnitMap[token.charAt(0)];
2937
		// are the dates the same for this unit of measurement?
2938
		if (unit && date1.isSame(date2, unit)) {
2939
			return momentFormat(date1, token); // would be the same if we used `date2`
2940
			// BTW, don't support custom tokens
2941
		}
2942
	}
2943
2944
	return false; // the chunk is NOT the same for the two dates
2945
	// BTW, don't support splitting on non-zero areas
2946
}
2947
2948
2949
// Chunking Utils
2950
// -------------------------------------------------------------------------------------------------
2951
2952
2953
var formatStringChunkCache = {};
2954
2955
2956
function getFormatStringChunks(formatStr) {
2957
	if (formatStr in formatStringChunkCache) {
2958
		return formatStringChunkCache[formatStr];
2959
	}
2960
	return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr));
2961
}
2962
2963
2964
// Break the formatting string into an array of chunks
2965
function chunkFormatString(formatStr) {
2966
	var chunks = [];
2967
	var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination
2968
	var match;
2969
2970
	while ((match = chunker.exec(formatStr))) {
2971
		if (match[1]) { // a literal string inside [ ... ]
2972
			chunks.push(match[1]);
2973
		}
2974
		else if (match[2]) { // non-zero formatting inside ( ... )
2975
			chunks.push({ maybe: chunkFormatString(match[2]) });
2976
		}
2977
		else if (match[3]) { // a formatting token
2978
			chunks.push({ token: match[3] });
2979
		}
2980
		else if (match[5]) { // an unenclosed literal string
2981
			chunks.push(match[5]);
2982
		}
2983
	}
2984
2985
	return chunks;
2986
}
2987
2988
;;
2989
2990
/* A rectangular panel that is absolutely positioned over other content
2991
------------------------------------------------------------------------------------------------------------------------
2992
Options:
2993
	- className (string)
2994
	- content (HTML string or jQuery element set)
2995
	- parentEl
2996
	- top
2997
	- left
2998
	- right (the x coord of where the right edge should be. not a "CSS" right)
2999
	- autoHide (boolean)
3000
	- show (callback)
3001
	- hide (callback)
3002
*/
3003
3004
function Popover(options) {
3005
	this.options = options || {};
3006
}
3007
3008
3009
Popover.prototype = {
3010
3011
	isHidden: true,
3012
	options: null,
3013
	el: null, // the container element for the popover. generated by this object
3014
	documentMousedownProxy: null, // document mousedown handler bound to `this`
3015
	margin: 10, // the space required between the popover and the edges of the scroll container
3016
3017
3018
	// Shows the popover on the specified position. Renders it if not already
3019
	show: function() {
3020
		if (this.isHidden) {
3021
			if (!this.el) {
3022
				this.render();
3023
			}
3024
			this.el.show();
3025
			this.position();
3026
			this.isHidden = false;
3027
			this.trigger('show');
3028
		}
3029
	},
3030
3031
3032
	// Hides the popover, through CSS, but does not remove it from the DOM
3033
	hide: function() {
3034
		if (!this.isHidden) {
3035
			this.el.hide();
3036
			this.isHidden = true;
3037
			this.trigger('hide');
3038
		}
3039
	},
3040
3041
3042
	// Creates `this.el` and renders content inside of it
3043
	render: function() {
3044
		var _this = this;
3045
		var options = this.options;
3046
3047
		this.el = $('<div class="fc-popover"/>')
3048
			.addClass(options.className || '')
3049
			.css({
3050
				// position initially to the top left to avoid creating scrollbars
3051
				top: 0,
3052
				left: 0
3053
			})
3054
			.append(options.content)
3055
			.appendTo(options.parentEl);
3056
3057
		// when a click happens on anything inside with a 'fc-close' className, hide the popover
3058
		this.el.on('click', '.fc-close', function() {
3059
			_this.hide();
3060
		});
3061
3062
		if (options.autoHide) {
3063
			$(document).on('mousedown', this.documentMousedownProxy = $.proxy(this, 'documentMousedown'));
3064
		}
3065
	},
3066
3067
3068
	// Triggered when the user clicks *anywhere* in the document, for the autoHide feature
3069
	documentMousedown: function(ev) {
3070
		// only hide the popover if the click happened outside the popover
3071
		if (this.el && !$(ev.target).closest(this.el).length) {
3072
			this.hide();
3073
		}
3074
	},
3075
3076
3077
	// Hides and unregisters any handlers
3078
	destroy: function() {
3079
		this.hide();
3080
3081
		if (this.el) {
3082
			this.el.remove();
3083
			this.el = null;
3084
		}
3085
3086
		$(document).off('mousedown', this.documentMousedownProxy);
3087
	},
3088
3089
3090
	// Positions the popover optimally, using the top/left/right options
3091
	position: function() {
3092
		var options = this.options;
3093
		var origin = this.el.offsetParent().offset();
3094
		var width = this.el.outerWidth();
3095
		var height = this.el.outerHeight();
3096
		var windowEl = $(window);
3097
		var viewportEl = getScrollParent(this.el);
3098
		var viewportTop;
3099
		var viewportLeft;
3100
		var viewportOffset;
3101
		var top; // the "position" (not "offset") values for the popover
3102
		var left; //
3103
3104
		// compute top and left
3105
		top = options.top || 0;
3106
		if (options.left !== undefined) {
3107
			left = options.left;
3108
		}
3109
		else if (options.right !== undefined) {
3110
			left = options.right - width; // derive the left value from the right value
3111
		}
3112
		else {
3113
			left = 0;
3114
		}
3115
3116
		if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result
3117
			viewportEl = windowEl;
3118
			viewportTop = 0; // the window is always at the top left
3119
			viewportLeft = 0; // (and .offset() won't work if called here)
3120
		}
3121
		else {
3122
			viewportOffset = viewportEl.offset();
3123
			viewportTop = viewportOffset.top;
3124
			viewportLeft = viewportOffset.left;
3125
		}
3126
3127
		// if the window is scrolled, it causes the visible area to be further down
3128
		viewportTop += windowEl.scrollTop();
3129
		viewportLeft += windowEl.scrollLeft();
3130
3131
		// constrain to the view port. if constrained by two edges, give precedence to top/left
3132
		if (options.viewportConstrain !== false) {
3133
			top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin);
3134
			top = Math.max(top, viewportTop + this.margin);
3135
			left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin);
3136
			left = Math.max(left, viewportLeft + this.margin);
3137
		}
3138
3139
		this.el.css({
3140
			top: top - origin.top,
3141
			left: left - origin.left
3142
		});
3143
	},
3144
3145
3146
	// Triggers a callback. Calls a function in the option hash of the same name.
3147
	// Arguments beyond the first `name` are forwarded on.
3148
	// TODO: better code reuse for this. Repeat code
3149
	trigger: function(name) {
3150
		if (this.options[name]) {
3151
			this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
3152
		}
3153
	}
3154
3155
};
3156
3157
;;
3158
3159
/* A "coordinate map" converts pixel coordinates into an associated cell, which has an associated date
3160
------------------------------------------------------------------------------------------------------------------------
3161
Common interface:
3162
3163
	CoordMap.prototype = {
3164
		build: function() {},
3165
		getCell: function(x, y) {}
3166
	};
3167
3168
*/
3169
3170
/* Coordinate map for a grid component
3171
----------------------------------------------------------------------------------------------------------------------*/
3172
3173
function GridCoordMap(grid) {
3174
	this.grid = grid;
3175
}
3176
3177
3178
GridCoordMap.prototype = {
3179
3180
	grid: null, // reference to the Grid
3181
	rows: null, // the top-to-bottom y coordinates. including the bottom of the last item
3182
	cols: null, // the left-to-right x coordinates. including the right of the last item
3183
3184
	containerEl: null, // container element that all coordinates are constrained to. optionally assigned
3185
	minX: null,
3186
	maxX: null, // exclusive
3187
	minY: null,
3188
	maxY: null, // exclusive
3189
3190
3191
	// Queries the grid for the coordinates of all the cells
3192
	build: function() {
3193
		this.grid.buildCoords(
3194
			this.rows = [],
3195
			this.cols = []
3196
		);
3197
		this.computeBounds();
3198
	},
3199
3200
3201
	// Given a coordinate of the document, gets the associated cell. If no cell is underneath, returns null
3202
	getCell: function(x, y) {
3203
		var cell = null;
3204
		var rows = this.rows;
3205
		var cols = this.cols;
3206
		var r = -1;
3207
		var c = -1;
3208
		var i;
3209
3210
		if (this.inBounds(x, y)) {
3211
3212
			for (i = 0; i < rows.length; i++) {
3213
				if (y >= rows[i][0] && y < rows[i][1]) {
3214
					r = i;
3215
					break;
3216
				}
3217
			}
3218
3219
			for (i = 0; i < cols.length; i++) {
3220
				if (x >= cols[i][0] && x < cols[i][1]) {
3221
					c = i;
3222
					break;
3223
				}
3224
			}
3225
3226
			if (r >= 0 && c >= 0) {
3227
				cell = { row: r, col: c };
3228
				cell.grid = this.grid;
3229
				cell.date = this.grid.getCellDate(cell);
3230
			}
3231
		}
3232
3233
		return cell;
3234
	},
3235
3236
3237
	// If there is a containerEl, compute the bounds into min/max values
3238
	computeBounds: function() {
3239
		var containerOffset;
3240
3241
		if (this.containerEl) {
3242
			containerOffset = this.containerEl.offset();
3243
			this.minX = containerOffset.left;
3244
			this.maxX = containerOffset.left + this.containerEl.outerWidth();
3245
			this.minY = containerOffset.top;
3246
			this.maxY = containerOffset.top + this.containerEl.outerHeight();
3247
		}
3248
	},
3249
3250
3251
	// Determines if the given coordinates are in bounds. If no `containerEl`, always true
3252
	inBounds: function(x, y) {
3253
		if (this.containerEl) {
3254
			return x >= this.minX && x < this.maxX && y >= this.minY && y < this.maxY;
3255
		}
3256
		return true;
3257
	}
3258
3259
};
3260
3261
3262
/* Coordinate map that is a combination of multiple other coordinate maps
3263
----------------------------------------------------------------------------------------------------------------------*/
3264
3265
function ComboCoordMap(coordMaps) {
3266
	this.coordMaps = coordMaps;
3267
}
3268
3269
3270
ComboCoordMap.prototype = {
3271
3272
	coordMaps: null, // an array of CoordMaps
3273
3274
3275
	// Builds all coordMaps
3276
	build: function() {
3277
		var coordMaps = this.coordMaps;
3278
		var i;
3279
3280
		for (i = 0; i < coordMaps.length; i++) {
3281
			coordMaps[i].build();
3282
		}
3283
	},
3284
3285
3286
	// Queries all coordMaps for the cell underneath the given coordinates, returning the first result
3287
	getCell: function(x, y) {
3288
		var coordMaps = this.coordMaps;
3289
		var cell = null;
3290
		var i;
3291
3292
		for (i = 0; i < coordMaps.length && !cell; i++) {
3293
			cell = coordMaps[i].getCell(x, y);
3294
		}
3295
3296
		return cell;
3297
	}
3298
3299
};
3300
3301
;;
3302
3303
/* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over.
3304
----------------------------------------------------------------------------------------------------------------------*/
3305
// TODO: implement scrolling
3306
3307
function DragListener(coordMap, options) {
3308
	this.coordMap = coordMap;
3309
	this.options = options || {};
3310
}
3311
3312
3313
DragListener.prototype = {
3314
3315
	coordMap: null,
3316
	options: null,
3317
3318
	isListening: false,
3319
	isDragging: false,
3320
3321
	// the cell/date the mouse was over when listening started
3322
	origCell: null,
3323
	origDate: null,
3324
3325
	// the cell/date the mouse is over
3326
	cell: null,
3327
	date: null,
3328
3329
	// coordinates of the initial mousedown
3330
	mouseX0: null,
3331
	mouseY0: null,
3332
3333
	// handler attached to the document, bound to the DragListener's `this`
3334
	mousemoveProxy: null,
3335
	mouseupProxy: null,
3336
3337
	scrollEl: null,
3338
	scrollBounds: null, // { top, bottom, left, right }
3339
	scrollTopVel: null, // pixels per second
3340
	scrollLeftVel: null, // pixels per second
3341
	scrollIntervalId: null, // ID of setTimeout for scrolling animation loop
3342
	scrollHandlerProxy: null, // this-scoped function for handling when scrollEl is scrolled
3343
3344
	scrollSensitivity: 30, // pixels from edge for scrolling to start
3345
	scrollSpeed: 200, // pixels per second, at maximum speed
3346
	scrollIntervalMs: 50, // millisecond wait between scroll increment
3347
3348
3349
	// Call this when the user does a mousedown. Will probably lead to startListening
3350
	mousedown: function(ev) {
3351
		if (isPrimaryMouseButton(ev)) {
3352
3353
			ev.preventDefault(); // prevents native selection in most browsers
3354
3355
			this.startListening(ev);
3356
3357
			// start the drag immediately if there is no minimum distance for a drag start
3358
			if (!this.options.distance) {
3359
				this.startDrag(ev);
3360
			}
3361
		}
3362
	},
3363
3364
3365
	// Call this to start tracking mouse movements
3366
	startListening: function(ev) {
3367
		var scrollParent;
3368
		var cell;
3369
3370
		if (!this.isListening) {
3371
3372
			// grab scroll container and attach handler
3373
			if (ev && this.options.scroll) {
3374
				scrollParent = getScrollParent($(ev.target));
3375
				if (!scrollParent.is(window) && !scrollParent.is(document)) {
3376
					this.scrollEl = scrollParent;
3377
3378
					// scope to `this`, and use `debounce` to make sure rapid calls don't happen
3379
					this.scrollHandlerProxy = debounce($.proxy(this, 'scrollHandler'), 100);
3380
					this.scrollEl.on('scroll', this.scrollHandlerProxy);
3381
				}
3382
			}
3383
3384
			this.computeCoords(); // relies on `scrollEl`
3385
3386
			// get info on the initial cell, date, and coordinates
3387
			if (ev) {
3388
				cell = this.getCell(ev);
3389
				this.origCell = cell;
3390
				this.origDate = cell ? cell.date : null;
3391
3392
				this.mouseX0 = ev.pageX;
3393
				this.mouseY0 = ev.pageY;
3394
			}
3395
3396
			$(document)
3397
				.on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove'))
3398
				.on('mouseup', this.mouseupProxy = $.proxy(this, 'mouseup'))
3399
				.on('selectstart', this.preventDefault); // prevents native selection in IE<=8
3400
3401
			this.isListening = true;
3402
			this.trigger('listenStart', ev);
3403
		}
3404
	},
3405
3406
3407
	// Recomputes the drag-critical positions of elements
3408
	computeCoords: function() {
3409
		this.coordMap.build();
3410
		this.computeScrollBounds();
3411
	},
3412
3413
3414
	// Called when the user moves the mouse
3415
	mousemove: function(ev) {
3416
		var minDistance;
3417
		var distanceSq; // current distance from mouseX0/mouseY0, squared
3418
3419
		if (!this.isDragging) { // if not already dragging...
3420
			// then start the drag if the minimum distance criteria is met
3421
			minDistance = this.options.distance || 1;
3422
			distanceSq = Math.pow(ev.pageX - this.mouseX0, 2) + Math.pow(ev.pageY - this.mouseY0, 2);
3423
			if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
3424
				this.startDrag(ev);
3425
			}
3426
		}
3427
3428
		if (this.isDragging) {
3429
			this.drag(ev); // report a drag, even if this mousemove initiated the drag
3430
		}
3431
	},
3432
3433
3434
	// Call this to initiate a legitimate drag.
3435
	// This function is called internally from this class, but can also be called explicitly from outside
3436
	startDrag: function(ev) {
3437
		var cell;
3438
3439
		if (!this.isListening) { // startDrag must have manually initiated
3440
			this.startListening();
3441
		}
3442
3443
		if (!this.isDragging) {
3444
			this.isDragging = true;
3445
			this.trigger('dragStart', ev);
3446
3447
			// report the initial cell the mouse is over
3448
			cell = this.getCell(ev);
3449
			if (cell) {
3450
				this.cellOver(cell, true);
3451
			}
3452
		}
3453
	},
3454
3455
3456
	// Called while the mouse is being moved and when we know a legitimate drag is taking place
3457
	drag: function(ev) {
3458
		var cell;
3459
3460
		if (this.isDragging) {
3461
			cell = this.getCell(ev);
3462
3463
			if (!isCellsEqual(cell, this.cell)) { // a different cell than before?
3464
				if (this.cell) {
3465
					this.cellOut();
3466
				}
3467
				if (cell) {
3468
					this.cellOver(cell);
3469
				}
3470
			}
3471
3472
			this.dragScroll(ev); // will possibly cause scrolling
3473
		}
3474
	},
3475
3476
3477
	// Called when a the mouse has just moved over a new cell
3478
	cellOver: function(cell) {
3479
		this.cell = cell;
3480
		this.date = cell.date;
3481
		this.trigger('cellOver', cell, cell.date);
3482
	},
3483
3484
3485
	// Called when the mouse has just moved out of a cell
3486
	cellOut: function() {
3487
		if (this.cell) {
3488
			this.trigger('cellOut', this.cell);
3489
			this.cell = null;
3490
			this.date = null;
3491
		}
3492
	},
3493
3494
3495
	// Called when the user does a mouseup
3496
	mouseup: function(ev) {
3497
		this.stopDrag(ev);
3498
		this.stopListening(ev);
3499
	},
3500
3501
3502
	// Called when the drag is over. Will not cause listening to stop however.
3503
	// A concluding 'cellOut' event will NOT be triggered.
3504
	stopDrag: function(ev) {
3505
		if (this.isDragging) {
3506
			this.stopScrolling();
3507
			this.trigger('dragStop', ev);
3508
			this.isDragging = false;
3509
		}
3510
	},
3511
3512
3513
	// Call this to stop listening to the user's mouse events
3514
	stopListening: function(ev) {
3515
		if (this.isListening) {
3516
3517
			// remove the scroll handler if there is a scrollEl
3518
			if (this.scrollEl) {
3519
				this.scrollEl.off('scroll', this.scrollHandlerProxy);
3520
				this.scrollHandlerProxy = null;
3521
			}
3522
3523
			$(document)
3524
				.off('mousemove', this.mousemoveProxy)
3525
				.off('mouseup', this.mouseupProxy)
3526
				.off('selectstart', this.preventDefault);
3527
3528
			this.mousemoveProxy = null;
3529
			this.mouseupProxy = null;
3530
3531
			this.isListening = false;
3532
			this.trigger('listenStop', ev);
3533
3534
			this.origCell = this.cell = null;
3535
			this.origDate = this.date = null;
3536
		}
3537
	},
3538
3539
3540
	// Gets the cell underneath the coordinates for the given mouse event
3541
	getCell: function(ev) {
3542
		return this.coordMap.getCell(ev.pageX, ev.pageY);
3543
	},
3544
3545
3546
	// Triggers a callback. Calls a function in the option hash of the same name.
3547
	// Arguments beyond the first `name` are forwarded on.
3548
	trigger: function(name) {
3549
		if (this.options[name]) {
3550
			this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
3551
		}
3552
	},
3553
3554
3555
	// Stops a given mouse event from doing it's native browser action. In our case, text selection.
3556
	preventDefault: function(ev) {
3557
		ev.preventDefault();
3558
	},
3559
3560
3561
	/* Scrolling
3562
	------------------------------------------------------------------------------------------------------------------*/
3563
3564
3565
	// Computes and stores the bounding rectangle of scrollEl
3566
	computeScrollBounds: function() {
3567
		var el = this.scrollEl;
3568
		var offset;
3569
3570
		if (el) {
3571
			offset = el.offset();
3572
			this.scrollBounds = {
3573
				top: offset.top,
3574
				left: offset.left,
3575
				bottom: offset.top + el.outerHeight(),
3576
				right: offset.left + el.outerWidth()
3577
			};
3578
		}
3579
	},
3580
3581
3582
	// Called when the dragging is in progress and scrolling should be updated
3583
	dragScroll: function(ev) {
3584
		var sensitivity = this.scrollSensitivity;
3585
		var bounds = this.scrollBounds;
3586
		var topCloseness, bottomCloseness;
3587
		var leftCloseness, rightCloseness;
3588
		var topVel = 0;
3589
		var leftVel = 0;
3590
3591
		if (bounds) { // only scroll if scrollEl exists
3592
3593
			// compute closeness to edges. valid range is from 0.0 - 1.0
3594
			topCloseness = (sensitivity - (ev.pageY - bounds.top)) / sensitivity;
3595
			bottomCloseness = (sensitivity - (bounds.bottom - ev.pageY)) / sensitivity;
3596
			leftCloseness = (sensitivity - (ev.pageX - bounds.left)) / sensitivity;
3597
			rightCloseness = (sensitivity - (bounds.right - ev.pageX)) / sensitivity;
3598
3599
			// translate vertical closeness into velocity.
3600
			// mouse must be completely in bounds for velocity to happen.
3601
			if (topCloseness >= 0 && topCloseness <= 1) {
3602
				topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up
3603
			}
3604
			else if (bottomCloseness >= 0 && bottomCloseness <= 1) {
3605
				topVel = bottomCloseness * this.scrollSpeed;
3606
			}
3607
3608
			// translate horizontal closeness into velocity
3609
			if (leftCloseness >= 0 && leftCloseness <= 1) {
3610
				leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left
3611
			}
3612
			else if (rightCloseness >= 0 && rightCloseness <= 1) {
3613
				leftVel = rightCloseness * this.scrollSpeed;
3614
			}
3615
		}
3616
3617
		this.setScrollVel(topVel, leftVel);
3618
	},
3619
3620
3621
	// Sets the speed-of-scrolling for the scrollEl
3622
	setScrollVel: function(topVel, leftVel) {
3623
3624
		this.scrollTopVel = topVel;
3625
		this.scrollLeftVel = leftVel;
3626
3627
		this.constrainScrollVel(); // massages into realistic values
3628
3629
		// if there is non-zero velocity, and an animation loop hasn't already started, then START
3630
		if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) {
3631
			this.scrollIntervalId = setInterval(
3632
				$.proxy(this, 'scrollIntervalFunc'), // scope to `this`
3633
				this.scrollIntervalMs
3634
			);
3635
		}
3636
	},
3637
3638
3639
	// Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way
3640
	constrainScrollVel: function() {
3641
		var el = this.scrollEl;
3642
3643
		if (this.scrollTopVel < 0) { // scrolling up?
3644
			if (el.scrollTop() <= 0) { // already scrolled all the way up?
3645
				this.scrollTopVel = 0;
3646
			}
3647
		}
3648
		else if (this.scrollTopVel > 0) { // scrolling down?
3649
			if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down?
3650
				this.scrollTopVel = 0;
3651
			}
3652
		}
3653
3654
		if (this.scrollLeftVel < 0) { // scrolling left?
3655
			if (el.scrollLeft() <= 0) { // already scrolled all the left?
3656
				this.scrollLeftVel = 0;
3657
			}
3658
		}
3659
		else if (this.scrollLeftVel > 0) { // scrolling right?
3660
			if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right?
3661
				this.scrollLeftVel = 0;
3662
			}
3663
		}
3664
	},
3665
3666
3667
	// This function gets called during every iteration of the scrolling animation loop
3668
	scrollIntervalFunc: function() {
3669
		var el = this.scrollEl;
3670
		var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by
3671
3672
		// change the value of scrollEl's scroll
3673
		if (this.scrollTopVel) {
3674
			el.scrollTop(el.scrollTop() + this.scrollTopVel * frac);
3675
		}
3676
		if (this.scrollLeftVel) {
3677
			el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac);
3678
		}
3679
3680
		this.constrainScrollVel(); // since the scroll values changed, recompute the velocities
3681
3682
		// if scrolled all the way, which causes the vels to be zero, stop the animation loop
3683
		if (!this.scrollTopVel && !this.scrollLeftVel) {
3684
			this.stopScrolling();
3685
		}
3686
	},
3687
3688
3689
	// Kills any existing scrolling animation loop
3690
	stopScrolling: function() {
3691
		if (this.scrollIntervalId) {
3692
			clearInterval(this.scrollIntervalId);
3693
			this.scrollIntervalId = null;
3694
3695
			// when all done with scrolling, recompute positions since they probably changed
3696
			this.computeCoords();
3697
		}
3698
	},
3699
3700
3701
	// Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
3702
	scrollHandler: function() {
3703
		// recompute all coordinates, but *only* if this is *not* part of our scrolling animation
3704
		if (!this.scrollIntervalId) {
3705
			this.computeCoords();
3706
		}
3707
	}
3708
3709
};
3710
3711
3712
// Returns `true` if the cells are identically equal. `false` otherwise.
3713
// They must have the same row, col, and be from the same grid.
3714
// Two null values will be considered equal, as two "out of the grid" states are the same.
3715
function isCellsEqual(cell1, cell2) {
3716
3717
	if (!cell1 && !cell2) {
3718
		return true;
3719
	}
3720
3721
	if (cell1 && cell2) {
3722
		return cell1.grid === cell2.grid &&
3723
			cell1.row === cell2.row &&
3724
			cell1.col === cell2.col;
3725
	}
3726
3727
	return false;
3728
}
3729
3730
;;
3731
3732
/* Creates a clone of an element and lets it track the mouse as it moves
3733
----------------------------------------------------------------------------------------------------------------------*/
3734
3735
function MouseFollower(sourceEl, options) {
3736
	this.options = options = options || {};
3737
	this.sourceEl = sourceEl;
3738
	this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent
3739
}
3740
3741
3742
MouseFollower.prototype = {
3743
3744
	options: null,
3745
3746
	sourceEl: null, // the element that will be cloned and made to look like it is dragging
3747
	el: null, // the clone of `sourceEl` that will track the mouse
3748
	parentEl: null, // the element that `el` (the clone) will be attached to
3749
3750
	// the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl
3751
	top0: null,
3752
	left0: null,
3753
3754
	// the initial position of the mouse
3755
	mouseY0: null,
3756
	mouseX0: null,
3757
3758
	// the number of pixels the mouse has moved from its initial position
3759
	topDelta: null,
3760
	leftDelta: null,
3761
3762
	mousemoveProxy: null, // document mousemove handler, bound to the MouseFollower's `this`
3763
3764
	isFollowing: false,
3765
	isHidden: false,
3766
	isAnimating: false, // doing the revert animation?
3767
3768
3769
	// Causes the element to start following the mouse
3770
	start: function(ev) {
3771
		if (!this.isFollowing) {
3772
			this.isFollowing = true;
3773
3774
			this.mouseY0 = ev.pageY;
3775
			this.mouseX0 = ev.pageX;
3776
			this.topDelta = 0;
3777
			this.leftDelta = 0;
3778
3779
			if (!this.isHidden) {
3780
				this.updatePosition();
3781
			}
3782
3783
			$(document).on('mousemove', this.mousemoveProxy = $.proxy(this, 'mousemove'));
3784
		}
3785
	},
3786
3787
3788
	// Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position.
3789
	// `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately.
3790
	stop: function(shouldRevert, callback) {
3791
		var _this = this;
3792
		var revertDuration = this.options.revertDuration;
3793
3794
		function complete() {
3795
			this.isAnimating = false;
3796
			_this.destroyEl();
3797
3798
			this.top0 = this.left0 = null; // reset state for future updatePosition calls
3799
3800
			if (callback) {
3801
				callback();
3802
			}
3803
		}
3804
3805
		if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time
3806
			this.isFollowing = false;
3807
3808
			$(document).off('mousemove', this.mousemoveProxy);
3809
3810
			if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation?
3811
				this.isAnimating = true;
3812
				this.el.animate({
3813
					top: this.top0,
3814
					left: this.left0
3815
				}, {
3816
					duration: revertDuration,
3817
					complete: complete
3818
				});
3819
			}
3820
			else {
3821
				complete();
3822
			}
3823
		}
3824
	},
3825
3826
3827
	// Gets the tracking element. Create it if necessary
3828
	getEl: function() {
3829
		var el = this.el;
3830
3831
		if (!el) {
3832
			this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
3833
			el = this.el = this.sourceEl.clone()
3834
				.css({
3835
					position: 'absolute',
3836
					visibility: '', // in case original element was hidden (commonly through hideEvents())
3837
					display: this.isHidden ? 'none' : '', // for when initially hidden
3838
					margin: 0,
3839
					right: 'auto', // erase and set width instead
3840
					bottom: 'auto', // erase and set height instead
3841
					width: this.sourceEl.width(), // explicit height in case there was a 'right' value
3842
					height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value
3843
					opacity: this.options.opacity || '',
3844
					zIndex: this.options.zIndex
3845
				})
3846
				.appendTo(this.parentEl);
3847
		}
3848
3849
		return el;
3850
	},
3851
3852
3853
	// Removes the tracking element if it has already been created
3854
	destroyEl: function() {
3855
		if (this.el) {
3856
			this.el.remove();
3857
			this.el = null;
3858
		}
3859
	},
3860
3861
3862
	// Update the CSS position of the tracking element
3863
	updatePosition: function() {
3864
		var sourceOffset;
3865
		var origin;
3866
3867
		this.getEl(); // ensure this.el
3868
3869
		// make sure origin info was computed
3870
		if (this.top0 === null) {
3871
			this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
3872
			sourceOffset = this.sourceEl.offset();
3873
			origin = this.el.offsetParent().offset();
3874
			this.top0 = sourceOffset.top - origin.top;
3875
			this.left0 = sourceOffset.left - origin.left;
3876
		}
3877
3878
		this.el.css({
3879
			top: this.top0 + this.topDelta,
3880
			left: this.left0 + this.leftDelta
3881
		});
3882
	},
3883
3884
3885
	// Gets called when the user moves the mouse
3886
	mousemove: function(ev) {
3887
		this.topDelta = ev.pageY - this.mouseY0;
3888
		this.leftDelta = ev.pageX - this.mouseX0;
3889
3890
		if (!this.isHidden) {
3891
			this.updatePosition();
3892
		}
3893
	},
3894
3895
3896
	// Temporarily makes the tracking element invisible. Can be called before following starts
3897
	hide: function() {
3898
		if (!this.isHidden) {
3899
			this.isHidden = true;
3900
			if (this.el) {
3901
				this.el.hide();
3902
			}
3903
		}
3904
	},
3905
3906
3907
	// Show the tracking element after it has been temporarily hidden
3908
	show: function() {
3909
		if (this.isHidden) {
3910
			this.isHidden = false;
3911
			this.updatePosition();
3912
			this.getEl().show();
3913
		}
3914
	}
3915
3916
};
3917
3918
;;
3919
3920
/* A utility class for rendering <tr> rows.
3921
----------------------------------------------------------------------------------------------------------------------*/
3922
// It leverages methods of the subclass and the View to determine custom rendering behavior for each row "type"
3923
// (such as highlight rows, day rows, helper rows, etc).
3924
3925
function RowRenderer(view) {
3926
	this.view = view;
3927
}
3928
3929
3930
RowRenderer.prototype = {
3931
3932
	view: null, // a View object
3933
	cellHtml: '<td/>', // plain default HTML used for a cell when no other is available
3934
3935
3936
	// Renders the HTML for a row, leveraging custom cell-HTML-renderers based on the `rowType`.
3937
	// Also applies the "intro" and "outro" cells, which are specified by the subclass and views.
3938
	// `row` is an optional row number.
3939
	rowHtml: function(rowType, row) {
3940
		var view = this.view;
3941
		var renderCell = this.getHtmlRenderer('cell', rowType);
3942
		var cellHtml = '';
3943
		var col;
3944
		var date;
3945
3946
		row = row || 0;
3947
3948
		for (col = 0; col < view.colCnt; col++) {
3949
			date = view.cellToDate(row, col);
3950
			cellHtml += renderCell(row, col, date);
3951
		}
3952
3953
		cellHtml = this.bookendCells(cellHtml, rowType, row); // apply intro and outro
3954
3955
		return '<tr>' + cellHtml + '</tr>';
3956
	},
3957
3958
3959
	// Applies the "intro" and "outro" HTML to the given cells.
3960
	// Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro.
3961
	// `cells` can be an HTML string of <td>'s or a jQuery <tr> element
3962
	// `row` is an optional row number.
3963
	bookendCells: function(cells, rowType, row) {
3964
		var view = this.view;
3965
		var intro = this.getHtmlRenderer('intro', rowType)(row || 0);
3966
		var outro = this.getHtmlRenderer('outro', rowType)(row || 0);
3967
		var isRTL = view.opt('isRTL');
3968
		var prependHtml = isRTL ? outro : intro;
3969
		var appendHtml = isRTL ? intro : outro;
3970
3971
		if (typeof cells === 'string') {
3972
			return prependHtml + cells + appendHtml;
3973
		}
3974
		else { // a jQuery <tr> element
3975
			return cells.prepend(prependHtml).append(appendHtml);
3976
		}
3977
	},
3978
3979
3980
	// Returns an HTML-rendering function given a specific `rendererName` (like cell, intro, or outro) and a specific
3981
	// `rowType` (like day, eventSkeleton, helperSkeleton), which is optional.
3982
	// If a renderer for the specific rowType doesn't exist, it will fall back to a generic renderer.
3983
	// We will query the View object first for any custom rendering functions, then the methods of the subclass.
3984
	getHtmlRenderer: function(rendererName, rowType) {
3985
		var view = this.view;
3986
		var generalName; // like "cellHtml"
3987
		var specificName; // like "dayCellHtml". based on rowType
3988
		var provider; // either the View or the RowRenderer subclass, whichever provided the method
3989
		var renderer;
3990
3991
		generalName = rendererName + 'Html';
3992
		if (rowType) {
3993
			specificName = rowType + capitaliseFirstLetter(rendererName) + 'Html';
3994
		}
3995
3996
		if (specificName && (renderer = view[specificName])) {
3997
			provider = view;
3998
		}
3999
		else if (specificName && (renderer = this[specificName])) {
4000
			provider = this;
4001
		}
4002
		else if ((renderer = view[generalName])) {
4003
			provider = view;
4004
		}
4005
		else if ((renderer = this[generalName])) {
4006
			provider = this;
4007
		}
4008
4009
		if (typeof renderer === 'function') {
4010
			return function(row) {
4011
				return renderer.apply(provider, arguments) || ''; // use correct `this` and always return a string
0 ignored issues
show
Bug introduced by
The variable provider does not seem to be initialized in case renderer = this.generalName on line 4005 is false. Are you sure the function apply handles undefined variables?
Loading history...
4012
			};
4013
		}
4014
4015
		// the rendered can be a plain string as well. if not specified, always an empty string.
4016
		return function() {
4017
			return renderer || '';
4018
		};
4019
	}
4020
4021
};
4022
4023
;;
4024
4025
/* An abstract class comprised of a "grid" of cells that each represent a specific datetime
4026
----------------------------------------------------------------------------------------------------------------------*/
4027
4028
function Grid(view) {
4029
	RowRenderer.call(this, view); // call the super-constructor
4030
	this.coordMap = new GridCoordMap(this);
4031
}
4032
4033
4034
Grid.prototype = createObject(RowRenderer.prototype); // declare the super-class
4035
$.extend(Grid.prototype, {
4036
4037
	el: null, // the containing element
4038
	coordMap: null, // a GridCoordMap that converts pixel values to datetimes
4039
	cellDuration: null, // a cell's duration. subclasses must assign this ASAP
4040
4041
4042
	// Renders the grid into the `el` element.
4043
	// Subclasses should override and call this super-method when done.
4044
	render: function() {
4045
		this.bindHandlers();
4046
	},
4047
4048
4049
	// Called when the grid's resources need to be cleaned up
4050
	destroy: function() {
4051
		// subclasses can implement
4052
	},
4053
4054
4055
	/* Coordinates & Cells
4056
	------------------------------------------------------------------------------------------------------------------*/
4057
4058
4059
	// Populates the given empty arrays with the y and x coordinates of the cells
4060
	buildCoords: function(rows, cols) {
4061
		// subclasses must implement
4062
	},
4063
4064
4065
	// Given a cell object, returns the date for that cell
4066
	getCellDate: function(cell) {
4067
		// subclasses must implement
4068
	},
4069
4070
4071
	// Given a cell object, returns the element that represents the cell's whole-day
4072
	getCellDayEl: function(cell) {
4073
		// subclasses must implement
4074
	},
4075
4076
4077
	// Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects
4078
	rangeToSegs: function(start, end) {
4079
		// subclasses must implement
4080
	},
4081
4082
4083
	/* Handlers
4084
	------------------------------------------------------------------------------------------------------------------*/
4085
4086
4087
	// Attach handlers to `this.el`, using bubbling to listen to all ancestors.
4088
	// We don't need to undo any of this in a "destroy" method, because the view will simply remove `this.el` from the
4089
	// DOM and jQuery will be smart enough to garbage collect the handlers.
4090
	bindHandlers: function() {
4091
		var _this = this;
4092
4093
		this.el.on('mousedown', function(ev) {
4094
			if (
4095
				!$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link
4096
				!$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one)
4097
			) {
4098
				_this.dayMousedown(ev);
4099
			}
4100
		});
4101
4102
		this.bindSegHandlers(); // attach event-element-related handlers. in Grid.events.js
4103
	},
4104
4105
4106
	// Process a mousedown on an element that represents a day. For day clicking and selecting.
4107
	dayMousedown: function(ev) {
4108
		var _this = this;
4109
		var view = this.view;
4110
		var isSelectable = view.opt('selectable');
4111
		var dates = null; // the inclusive dates of the selection. will be null if no selection
4112
		var start; // the inclusive start of the selection
4113
		var end; // the *exclusive* end of the selection
4114
		var dayEl;
4115
4116
		// this listener tracks a mousedown on a day element, and a subsequent drag.
4117
		// if the drag ends on the same day, it is a 'dayClick'.
4118
		// if 'selectable' is enabled, this listener also detects selections.
4119
		var dragListener = new DragListener(this.coordMap, {
4120
			//distance: 5, // needs more work if we want dayClick to fire correctly
4121
			scroll: view.opt('dragScroll'),
4122
			dragStart: function() {
4123
				view.unselect(); // since we could be rendering a new selection, we want to clear any old one
4124
			},
4125
			cellOver: function(cell, date) {
4126
				if (dragListener.origDate) { // click needs to have started on a cell
4127
4128
					dayEl = _this.getCellDayEl(cell);
4129
4130
					dates = [ date, dragListener.origDate ].sort(dateCompare);
4131
					start = dates[0];
4132
					end = dates[1].clone().add(_this.cellDuration);
4133
4134
					if (isSelectable) {
4135
						_this.renderSelection(start, end);
4136
					}
4137
				}
4138
			},
4139
			cellOut: function(cell, date) {
4140
				dates = null;
4141
				_this.destroySelection();
4142
			},
4143
			listenStop: function(ev) {
4144
				if (dates) { // started and ended on a cell?
4145
					if (dates[0].isSame(dates[1])) {
4146
						view.trigger('dayClick', dayEl[0], start, ev);
4147
					}
4148
					if (isSelectable) {
4149
						// the selection will already have been rendered. just report it
4150
						view.reportSelection(start, end, ev);
4151
					}
4152
				}
4153
			}
4154
		});
4155
4156
		dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart
4157
	},
4158
4159
4160
	/* Event Dragging
4161
	------------------------------------------------------------------------------------------------------------------*/
4162
4163
4164
	// Renders a visual indication of a event being dragged over the given date(s).
4165
	// `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info.
4166
	// A returned value of `true` signals that a mock "helper" event has been rendered.
4167
	renderDrag: function(start, end, seg) {
4168
		// subclasses must implement
4169
	},
4170
4171
4172
	// Unrenders a visual indication of an event being dragged
4173
	destroyDrag: function() {
4174
		// subclasses must implement
4175
	},
4176
4177
4178
	/* Event Resizing
4179
	------------------------------------------------------------------------------------------------------------------*/
4180
4181
4182
	// Renders a visual indication of an event being resized.
4183
	// `start` and `end` are the updated dates of the event. `seg` is the original segment object involved in the drag.
4184
	renderResize: function(start, end, seg) {
4185
		// subclasses must implement
4186
	},
4187
4188
4189
	// Unrenders a visual indication of an event being resized.
4190
	destroyResize: function() {
4191
		// subclasses must implement
4192
	},
4193
4194
4195
	/* Event Helper
4196
	------------------------------------------------------------------------------------------------------------------*/
4197
4198
4199
	// Renders a mock event over the given date(s).
4200
	// `end` can be null, in which case the mock event that is rendered will have a null end time.
4201
	// `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging.
4202
	renderRangeHelper: function(start, end, sourceSeg) {
4203
		var view = this.view;
4204
		var fakeEvent;
4205
4206
		// compute the end time if forced to do so (this is what EventManager does)
4207
		if (!end && view.opt('forceEventDuration')) {
4208
			end = view.calendar.getDefaultEventEnd(!start.hasTime(), start);
4209
		}
4210
4211
		fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible
4212
		fakeEvent.start = start;
4213
		fakeEvent.end = end;
4214
		fakeEvent.allDay = !(start.hasTime() || (end && end.hasTime())); // freshly compute allDay
4215
4216
		// this extra className will be useful for differentiating real events from mock events in CSS
4217
		fakeEvent.className = (fakeEvent.className || []).concat('fc-helper');
4218
4219
		// if something external is being dragged in, don't render a resizer
4220
		if (!sourceSeg) {
4221
			fakeEvent.editable = false;
4222
		}
4223
4224
		this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
4225
	},
4226
4227
4228
	// Renders a mock event
4229
	renderHelper: function(event, sourceSeg) {
4230
		// subclasses must implement
4231
	},
4232
4233
4234
	// Unrenders a mock event
4235
	destroyHelper: function() {
4236
		// subclasses must implement
4237
	},
4238
4239
4240
	/* Selection
4241
	------------------------------------------------------------------------------------------------------------------*/
4242
4243
4244
	// Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.
4245
	renderSelection: function(start, end) {
4246
		this.renderHighlight(start, end);
4247
	},
4248
4249
4250
	// Unrenders any visual indications of a selection. Will unrender a highlight by default.
4251
	destroySelection: function() {
4252
		this.destroyHighlight();
4253
	},
4254
4255
4256
	/* Highlight
4257
	------------------------------------------------------------------------------------------------------------------*/
4258
4259
4260
	// Puts visual emphasis on a certain date range
4261
	renderHighlight: function(start, end) {
4262
		// subclasses should implement
4263
	},
4264
4265
4266
	// Removes visual emphasis on a date range
4267
	destroyHighlight: function() {
4268
		// subclasses should implement
4269
	},
4270
4271
4272
4273
	/* Generic rendering utilities for subclasses
4274
	------------------------------------------------------------------------------------------------------------------*/
4275
4276
4277
	// Renders a day-of-week header row
4278
	headHtml: function() {
4279
		return '' +
4280
			'<div class="fc-row ' + this.view.widgetHeaderClass + '">' +
4281
				'<table>' +
4282
					'<thead>' +
4283
						this.rowHtml('head') + // leverages RowRenderer
4284
					'</thead>' +
4285
				'</table>' +
4286
			'</div>';
4287
	},
4288
4289
4290
	// Used by the `headHtml` method, via RowRenderer, for rendering the HTML of a day-of-week header cell
4291
	headCellHtml: function(row, col, date) {
4292
		var view = this.view;
4293
		var calendar = view.calendar;
4294
		var colFormat = view.opt('columnFormat');
4295
4296
		return '' +
4297
			'<th class="fc-day-header ' + view.widgetHeaderClass + ' fc-' + dayIDs[date.day()] + '">' +
4298
				htmlEscape(calendar.formatDate(date, colFormat)) +
4299
			'</th>';
4300
	},
4301
4302
4303
	// Renders the HTML for a single-day background cell
4304
	bgCellHtml: function(row, col, date) {
4305
		var view = this.view;
4306
		var classes = this.getDayClasses(date);
4307
4308
		classes.unshift('fc-day', view.widgetContentClass);
4309
4310
		return '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '"></td>';
4311
	},
4312
4313
4314
	// Computes HTML classNames for a single-day cell
4315
	getDayClasses: function(date) {
4316
		var view = this.view;
4317
		var today = view.calendar.getNow().stripTime();
4318
		var classes = [ 'fc-' + dayIDs[date.day()] ];
4319
4320
		if (
4321
			view.name === 'month' &&
4322
			date.month() != view.intervalStart.month()
4323
		) {
4324
			classes.push('fc-other-month');
4325
		}
4326
4327
		if (date.isSame(today, 'day')) {
4328
			classes.push(
4329
				'fc-today',
4330
				view.highlightStateClass
4331
			);
4332
		}
4333
		else if (date < today) {
4334
			classes.push('fc-past');
4335
		}
4336
		else {
4337
			classes.push('fc-future');
4338
		}
4339
4340
		return classes;
4341
	}
4342
4343
});
4344
4345
;;
4346
4347
/* Event-rendering and event-interaction methods for the abstract Grid class
4348
----------------------------------------------------------------------------------------------------------------------*/
4349
4350
$.extend(Grid.prototype, {
4351
4352
	mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing
4353
	isDraggingSeg: false, // is a segment being dragged? boolean
4354
	isResizingSeg: false, // is a segment being resized? boolean
4355
4356
4357
	// Renders the given events onto the grid
4358
	renderEvents: function(events) {
4359
		// subclasses must implement
4360
	},
4361
4362
4363
	// Retrieves all rendered segment objects in this grid
4364
	getSegs: function() {
4365
		// subclasses must implement
4366
	},
4367
4368
4369
	// Unrenders all events. Subclasses should implement, calling this super-method first.
4370
	destroyEvents: function() {
4371
		this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
4372
	},
4373
4374
4375
	// Renders a `el` property for each seg, and only returns segments that successfully rendered
4376
	renderSegs: function(segs, disableResizing) {
4377
		var view = this.view;
4378
		var html = '';
4379
		var renderedSegs = [];
4380
		var i;
4381
4382
		// build a large concatenation of event segment HTML
4383
		for (i = 0; i < segs.length; i++) {
4384
			html += this.renderSegHtml(segs[i], disableResizing);
4385
		}
4386
4387
		// Grab individual elements from the combined HTML string. Use each as the default rendering.
4388
		// Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
4389
		$(html).each(function(i, node) {
4390
			var seg = segs[i];
4391
			var el = view.resolveEventEl(seg.event, $(node));
4392
			if (el) {
4393
				el.data('fc-seg', seg); // used by handlers
4394
				seg.el = el;
4395
				renderedSegs.push(seg);
4396
			}
4397
		});
4398
4399
		return renderedSegs;
4400
	},
4401
4402
4403
	// Generates the HTML for the default rendering of a segment
4404
	renderSegHtml: function(seg, disableResizing) {
4405
		// subclasses must implement
4406
	},
4407
4408
4409
	// Converts an array of event objects into an array of segment objects
4410
	eventsToSegs: function(events, intervalStart, intervalEnd) {
4411
		var _this = this;
4412
4413
		return $.map(events, function(event) {
4414
			return _this.eventToSegs(event, intervalStart, intervalEnd); // $.map flattens all returned arrays together
4415
		});
4416
	},
4417
4418
4419
	// Slices a single event into an array of event segments.
4420
	// When `intervalStart` and `intervalEnd` are specified, intersect the events with that interval.
4421
	// Otherwise, let the subclass decide how it wants to slice the segments over the grid.
4422
	eventToSegs: function(event, intervalStart, intervalEnd) {
4423
		var eventStart = event.start.clone().stripZone(); // normalize
4424
		var eventEnd = this.view.calendar.getEventEnd(event).stripZone(); // compute (if necessary) and normalize
4425
		var segs;
4426
		var i, seg;
4427
4428
		if (intervalStart && intervalEnd) {
4429
			seg = intersectionToSeg(eventStart, eventEnd, intervalStart, intervalEnd);
4430
			segs = seg ? [ seg ] : [];
4431
		}
4432
		else {
4433
			segs = this.rangeToSegs(eventStart, eventEnd); // defined by the subclass
4434
		}
4435
4436
		// assign extra event-related properties to the segment objects
4437
		for (i = 0; i < segs.length; i++) {
4438
			seg = segs[i];
4439
			seg.event = event;
4440
			seg.eventStartMS = +eventStart;
4441
			seg.eventDurationMS = eventEnd - eventStart;
4442
		}
4443
4444
		return segs;
4445
	},
4446
4447
4448
	/* Handlers
4449
	------------------------------------------------------------------------------------------------------------------*/
4450
4451
4452
	// Attaches event-element-related handlers to the container element and leverage bubbling
4453
	bindSegHandlers: function() {
4454
		var _this = this;
4455
		var view = this.view;
4456
4457
		$.each(
4458
			{
4459
				mouseenter: function(seg, ev) {
4460
					_this.triggerSegMouseover(seg, ev);
4461
				},
4462
				mouseleave: function(seg, ev) {
4463
					_this.triggerSegMouseout(seg, ev);
4464
				},
4465
				click: function(seg, ev) {
4466
					return view.trigger('eventClick', this, seg.event, ev); // can return `false` to cancel
4467
				},
4468
				mousedown: function(seg, ev) {
4469
					if ($(ev.target).is('.fc-resizer') && view.isEventResizable(seg.event)) {
4470
						_this.segResizeMousedown(seg, ev);
4471
					}
4472
					else if (view.isEventDraggable(seg.event)) {
4473
						_this.segDragMousedown(seg, ev);
4474
					}
4475
				}
4476
			},
4477
			function(name, func) {
4478
				// attach the handler to the container element and only listen for real event elements via bubbling
4479
				_this.el.on(name, '.fc-event-container > *', function(ev) {
4480
					var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
4481
4482
					// only call the handlers if there is not a drag/resize in progress
4483
					if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
4484
						return func.call(this, seg, ev); // `this` will be the event element
4485
					}
4486
				});
4487
			}
4488
		);
4489
	},
4490
4491
4492
	// Updates internal state and triggers handlers for when an event element is moused over
4493
	triggerSegMouseover: function(seg, ev) {
4494
		if (!this.mousedOverSeg) {
4495
			this.mousedOverSeg = seg;
4496
			this.view.trigger('eventMouseover', seg.el[0], seg.event, ev);
4497
		}
4498
	},
4499
4500
4501
	// Updates internal state and triggers handlers for when an event element is moused out.
4502
	// Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
4503
	triggerSegMouseout: function(seg, ev) {
4504
		ev = ev || {}; // if given no args, make a mock mouse event
4505
4506
		if (this.mousedOverSeg) {
4507
			seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment
4508
			this.mousedOverSeg = null;
4509
			this.view.trigger('eventMouseout', seg.el[0], seg.event, ev);
4510
		}
4511
	},
4512
4513
4514
	/* Dragging
4515
	------------------------------------------------------------------------------------------------------------------*/
4516
4517
4518
	// Called when the user does a mousedown on an event, which might lead to dragging.
4519
	// Generic enough to work with any type of Grid.
4520
	segDragMousedown: function(seg, ev) {
4521
		var _this = this;
4522
		var view = this.view;
4523
		var el = seg.el;
4524
		var event = seg.event;
4525
		var newStart, newEnd;
4526
4527
		// A clone of the original element that will move with the mouse
4528
		var mouseFollower = new MouseFollower(seg.el, {
4529
			parentEl: view.el,
4530
			opacity: view.opt('dragOpacity'),
4531
			revertDuration: view.opt('dragRevertDuration'),
4532
			zIndex: 2 // one above the .fc-view
4533
		});
4534
4535
		// Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
4536
		// of the view.
4537
		var dragListener = new DragListener(view.coordMap, {
4538
			distance: 5,
4539
			scroll: view.opt('dragScroll'),
4540
			listenStart: function(ev) {
4541
				mouseFollower.hide(); // don't show until we know this is a real drag
4542
				mouseFollower.start(ev);
4543
			},
4544
			dragStart: function(ev) {
4545
				_this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
4546
				_this.isDraggingSeg = true;
4547
				view.hideEvent(event); // hide all event segments. our mouseFollower will take over
4548
				view.trigger('eventDragStart', el[0], event, ev, {}); // last argument is jqui dummy
4549
			},
4550
			cellOver: function(cell, date) {
4551
				var origDate = seg.cellDate || dragListener.origDate;
4552
				var res = _this.computeDraggedEventDates(seg, origDate, date);
4553
				newStart = res.start;
4554
				newEnd = res.end;
4555
4556
				if (view.renderDrag(newStart, newEnd, seg)) { // have the view render a visual indication
4557
					mouseFollower.hide(); // if the view is already using a mock event "helper", hide our own
4558
				}
4559
				else {
4560
					mouseFollower.show();
4561
				}
4562
			},
4563
			cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells
4564
				newStart = null;
4565
				view.destroyDrag(); // unrender whatever was done in view.renderDrag
4566
				mouseFollower.show(); // show in case we are moving out of all cells
4567
			},
4568
			dragStop: function(ev) {
4569
				var hasChanged = newStart && !newStart.isSame(event.start);
4570
4571
				// do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
4572
				mouseFollower.stop(!hasChanged, function() {
4573
					_this.isDraggingSeg = false;
4574
					view.destroyDrag();
4575
					view.showEvent(event);
4576
					view.trigger('eventDragStop', el[0], event, ev, {}); // last argument is jqui dummy
4577
4578
					if (hasChanged) {
4579
						view.eventDrop(el[0], event, newStart, ev); // will rerender all events...
0 ignored issues
show
Bug introduced by
The variable newStart seems to not be initialized for all possible execution paths. Are you sure eventDrop handles undefined variables?
Loading history...
4580
					}
4581
				});
4582
			},
4583
			listenStop: function() {
4584
				mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started
4585
			}
4586
		});
4587
4588
		dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart
4589
	},
4590
4591
4592
	// Given a segment, the dates where a drag began and ended, calculates the Event Object's new start and end dates
4593
	computeDraggedEventDates: function(seg, dragStartDate, dropDate) {
4594
		var view = this.view;
4595
		var event = seg.event;
4596
		var start = event.start;
4597
		var end = view.calendar.getEventEnd(event);
4598
		var delta;
4599
		var newStart;
4600
		var newEnd;
4601
4602
		if (dropDate.hasTime() === dragStartDate.hasTime()) {
4603
			delta = dayishDiff(dropDate, dragStartDate);
4604
			newStart = start.clone().add(delta);
4605
			if (event.end === null) { // do we need to compute an end?
4606
				newEnd = null;
4607
			}
4608
			else {
4609
				newEnd = end.clone().add(delta);
4610
			}
4611
		}
4612
		else {
4613
			// if switching from day <-> timed, start should be reset to the dropped date, and the end cleared
4614
			newStart = dropDate;
4615
			newEnd = null; // end should be cleared
4616
		}
4617
4618
		return { start: newStart, end: newEnd };
4619
	},
4620
4621
4622
	/* Resizing
4623
	------------------------------------------------------------------------------------------------------------------*/
4624
4625
4626
	// Called when the user does a mousedown on an event's resizer, which might lead to resizing.
4627
	// Generic enough to work with any type of Grid.
4628
	segResizeMousedown: function(seg, ev) {
4629
		var _this = this;
4630
		var view = this.view;
4631
		var el = seg.el;
4632
		var event = seg.event;
4633
		var start = event.start;
4634
		var end = view.calendar.getEventEnd(event);
4635
		var newEnd = null;
4636
		var dragListener;
4637
4638
		function destroy() { // resets the rendering
4639
			_this.destroyResize();
4640
			view.showEvent(event);
4641
		}
4642
4643
		// Tracks mouse movement over the *grid's* coordinate map
4644
		dragListener = new DragListener(this.coordMap, {
4645
			distance: 5,
4646
			scroll: view.opt('dragScroll'),
4647
			dragStart: function(ev) {
4648
				_this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
4649
				_this.isResizingSeg = true;
4650
				view.trigger('eventResizeStart', el[0], event, ev, {}); // last argument is jqui dummy
4651
			},
4652
			cellOver: function(cell, date) {
4653
				// compute the new end. don't allow it to go before the event's start
4654
				if (date.isBefore(start)) { // allows comparing ambig to non-ambig
4655
					date = start;
4656
				}
4657
				newEnd = date.clone().add(_this.cellDuration); // make it an exclusive end
4658
4659
				if (newEnd.isSame(end)) {
4660
					newEnd = null;
4661
					destroy();
4662
				}
4663
				else {
4664
					_this.renderResize(start, newEnd, seg);
4665
					view.hideEvent(event);
4666
				}
4667
			},
4668
			cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells
4669
				newEnd = null;
4670
				destroy();
4671
			},
4672
			dragStop: function(ev) {
4673
				_this.isResizingSeg = false;
4674
				destroy();
4675
				view.trigger('eventResizeStop', el[0], event, ev, {}); // last argument is jqui dummy
4676
4677
				if (newEnd) {
4678
					view.eventResize(el[0], event, newEnd, ev); // will rerender all events...
4679
				}
4680
			}
4681
		});
4682
4683
		dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart
4684
	},
4685
4686
4687
	/* Rendering Utils
4688
	------------------------------------------------------------------------------------------------------------------*/
4689
4690
4691
	// Generic utility for generating the HTML classNames for an event segment's element
4692
	getSegClasses: function(seg, isDraggable, isResizable) {
4693
		var event = seg.event;
4694
		var classes = [
4695
			'fc-event',
4696
			seg.isStart ? 'fc-start' : 'fc-not-start',
4697
			seg.isEnd ? 'fc-end' : 'fc-not-end'
4698
		].concat(
4699
			event.className,
4700
			event.source ? event.source.className : []
4701
		);
4702
4703
		if (isDraggable) {
4704
			classes.push('fc-draggable');
4705
		}
4706
		if (isResizable) {
4707
			classes.push('fc-resizable');
4708
		}
4709
4710
		return classes;
4711
	},
4712
4713
4714
	// Utility for generating a CSS string with all the event skin-related properties
4715
	getEventSkinCss: function(event) {
4716
		var view = this.view;
4717
		var source = event.source || {};
4718
		var eventColor = event.color;
4719
		var sourceColor = source.color;
4720
		var optionColor = view.opt('eventColor');
4721
		var backgroundColor =
4722
			event.backgroundColor ||
4723
			eventColor ||
4724
			source.backgroundColor ||
4725
			sourceColor ||
4726
			view.opt('eventBackgroundColor') ||
4727
			optionColor;
4728
		var borderColor =
4729
			event.borderColor ||
4730
			eventColor ||
4731
			source.borderColor ||
4732
			sourceColor ||
4733
			view.opt('eventBorderColor') ||
4734
			optionColor;
4735
		var textColor =
4736
			event.textColor ||
4737
			source.textColor ||
4738
			view.opt('eventTextColor');
4739
		var statements = [];
4740
		if (backgroundColor) {
4741
			statements.push('background-color:' + backgroundColor);
4742
		}
4743
		if (borderColor) {
4744
			statements.push('border-color:' + borderColor);
4745
		}
4746
		if (textColor) {
4747
			statements.push('color:' + textColor);
4748
		}
4749
		return statements.join(';');
4750
	}
4751
4752
});
4753
4754
4755
/* Event Segment Utilities
4756
----------------------------------------------------------------------------------------------------------------------*/
4757
4758
4759
// A cmp function for determining which segments should take visual priority
4760
function compareSegs(seg1, seg2) {
4761
	return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first
4762
		seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first
4763
		seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1)
4764
		(seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title
4765
}
4766
4767
4768
;;
4769
4770
/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
4771
----------------------------------------------------------------------------------------------------------------------*/
4772
4773
function DayGrid(view) {
4774
	Grid.call(this, view); // call the super-constructor
4775
}
4776
4777
4778
DayGrid.prototype = createObject(Grid.prototype); // declare the super-class
4779
$.extend(DayGrid.prototype, {
4780
4781
	numbersVisible: false, // should render a row for day/week numbers? manually set by the view
4782
	cellDuration: moment.duration({ days: 1 }), // required for Grid.event.js. Each cell is always a single day
4783
	bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid
4784
4785
	rowEls: null, // set of fake row elements
4786
	dayEls: null, // set of whole-day elements comprising the row's background
4787
	helperEls: null, // set of cell skeleton elements for rendering the mock event "helper"
4788
	highlightEls: null, // set of cell skeleton elements for rendering the highlight
4789
4790
4791
	// Renders the rows and columns into the component's `this.el`, which should already be assigned.
4792
	// isRigid determins whether the individual rows should ignore the contents and be a constant height.
4793
	// Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.
4794
	render: function(isRigid) {
4795
		var view = this.view;
4796
		var html = '';
4797
		var row;
4798
4799
		for (row = 0; row < view.rowCnt; row++) {
4800
			html += this.dayRowHtml(row, isRigid);
4801
		}
4802
		this.el.html(html);
4803
4804
		this.rowEls = this.el.find('.fc-row');
4805
		this.dayEls = this.el.find('.fc-day');
4806
4807
		// run all the day cells through the dayRender callback
4808
		this.dayEls.each(function(i, node) {
4809
			var date = view.cellToDate(Math.floor(i / view.colCnt), i % view.colCnt);
4810
			view.trigger('dayRender', null, date, $(node));
4811
		});
4812
4813
		Grid.prototype.render.call(this); // call the super-method
4814
	},
4815
4816
4817
	destroy: function() {
4818
		this.destroySegPopover();
4819
	},
4820
4821
4822
	// Generates the HTML for a single row. `row` is the row number.
4823
	dayRowHtml: function(row, isRigid) {
4824
		var view = this.view;
4825
		var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ];
4826
4827
		if (isRigid) {
4828
			classes.push('fc-rigid');
4829
		}
4830
4831
		return '' +
4832
			'<div class="' + classes.join(' ') + '">' +
4833
				'<div class="fc-bg">' +
4834
					'<table>' +
4835
						this.rowHtml('day', row) + // leverages RowRenderer. calls dayCellHtml()
4836
					'</table>' +
4837
				'</div>' +
4838
				'<div class="fc-content-skeleton">' +
4839
					'<table>' +
4840
						(this.numbersVisible ?
4841
							'<thead>' +
4842
								this.rowHtml('number', row) + // leverages RowRenderer. View will define render method
4843
							'</thead>' :
4844
							''
4845
							) +
4846
					'</table>' +
4847
				'</div>' +
4848
			'</div>';
4849
	},
4850
4851
4852
	// Renders the HTML for a whole-day cell. Will eventually end up in the day-row's background.
4853
	// We go through a 'day' row type instead of just doing a 'bg' row type so that the View can do custom rendering
4854
	// specifically for whole-day rows, whereas a 'bg' might also be used for other purposes (TimeGrid bg for example).
4855
	dayCellHtml: function(row, col, date) {
4856
		return this.bgCellHtml(row, col, date);
4857
	},
4858
4859
4860
	/* Coordinates & Cells
4861
	------------------------------------------------------------------------------------------------------------------*/
4862
4863
4864
	// Populates the empty `rows` and `cols` arrays with coordinates of the cells. For CoordGrid.
4865
	buildCoords: function(rows, cols) {
4866
		var colCnt = this.view.colCnt;
4867
		var e, n, p;
4868
4869
		this.dayEls.slice(0, colCnt).each(function(i, _e) { // iterate the first row of day elements
4870
			e = $(_e);
4871
			n = e.offset().left;
4872
			if (i) {
4873
				p[1] = n;
4874
			}
4875
			p = [ n ];
4876
			cols[i] = p;
4877
		});
4878
		p[1] = n + e.outerWidth();
4879
4880
		this.rowEls.each(function(i, _e) {
4881
			e = $(_e);
4882
			n = e.offset().top;
4883
			if (i) {
4884
				p[1] = n;
4885
			}
4886
			p = [ n ];
4887
			rows[i] = p;
4888
		});
4889
		p[1] = n + e.outerHeight() + this.bottomCoordPadding; // hack to extend hit area of last row
4890
	},
4891
4892
4893
	// Converts a cell to a date
4894
	getCellDate: function(cell) {
4895
		return this.view.cellToDate(cell); // leverages the View's cell system
4896
	},
4897
4898
4899
	// Gets the whole-day element associated with the cell
4900
	getCellDayEl: function(cell) {
4901
		return this.dayEls.eq(cell.row * this.view.colCnt + cell.col);
4902
	},
4903
4904
4905
	// Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects
4906
	rangeToSegs: function(start, end) {
4907
		return this.view.rangeToSegments(start, end); // leverages the View's cell system
4908
	},
4909
4910
4911
	/* Event Drag Visualization
4912
	------------------------------------------------------------------------------------------------------------------*/
4913
4914
4915
	// Renders a visual indication of an event hovering over the given date(s).
4916
	// `end` can be null, as well as `seg`. See View's documentation on renderDrag for more info.
4917
	// A returned value of `true` signals that a mock "helper" event has been rendered.
4918
	renderDrag: function(start, end, seg) {
4919
		var opacity;
4920
4921
		// always render a highlight underneath
4922
		this.renderHighlight(
4923
			start,
4924
			end || this.view.calendar.getDefaultEventEnd(true, start)
4925
		);
4926
4927
		// if a segment from the same calendar but another component is being dragged, render a helper event
4928
		if (seg && !seg.el.closest(this.el).length) {
4929
4930
			this.renderRangeHelper(start, end, seg);
4931
4932
			opacity = this.view.opt('dragOpacity');
4933
			if (opacity !== undefined) {
4934
				this.helperEls.css('opacity', opacity);
4935
			}
4936
4937
			return true; // a helper has been rendered
4938
		}
4939
	},
4940
4941
4942
	// Unrenders any visual indication of a hovering event
4943
	destroyDrag: function() {
4944
		this.destroyHighlight();
4945
		this.destroyHelper();
4946
	},
4947
4948
4949
	/* Event Resize Visualization
4950
	------------------------------------------------------------------------------------------------------------------*/
4951
4952
4953
	// Renders a visual indication of an event being resized
4954
	renderResize: function(start, end, seg) {
4955
		this.renderHighlight(start, end);
4956
		this.renderRangeHelper(start, end, seg);
4957
	},
4958
4959
4960
	// Unrenders a visual indication of an event being resized
4961
	destroyResize: function() {
4962
		this.destroyHighlight();
4963
		this.destroyHelper();
4964
	},
4965
4966
4967
	/* Event Helper
4968
	------------------------------------------------------------------------------------------------------------------*/
4969
4970
4971
	// Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.
4972
	renderHelper: function(event, sourceSeg) {
4973
		var helperNodes = [];
4974
		var rowStructs = this.renderEventRows([ event ]);
4975
4976
		// inject each new event skeleton into each associated row
4977
		this.rowEls.each(function(row, rowNode) {
4978
			var rowEl = $(rowNode); // the .fc-row
4979
			var skeletonEl = $('<div class="fc-helper-skeleton"><table/></div>'); // will be absolutely positioned
4980
			var skeletonTop;
4981
4982
			// If there is an original segment, match the top position. Otherwise, put it at the row's top level
4983
			if (sourceSeg && sourceSeg.row === row) {
4984
				skeletonTop = sourceSeg.el.position().top;
4985
			}
4986
			else {
4987
				skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top;
4988
			}
4989
4990
			skeletonEl.css('top', skeletonTop)
4991
				.find('table')
4992
					.append(rowStructs[row].tbodyEl);
4993
4994
			rowEl.append(skeletonEl);
4995
			helperNodes.push(skeletonEl[0]);
4996
		});
4997
4998
		this.helperEls = $(helperNodes); // array -> jQuery set
4999
	},
5000
5001
5002
	// Unrenders any visual indication of a mock helper event
5003
	destroyHelper: function() {
5004
		if (this.helperEls) {
5005
			this.helperEls.remove();
5006
			this.helperEls = null;
5007
		}
5008
	},
5009
5010
5011
	/* Highlighting
5012
	------------------------------------------------------------------------------------------------------------------*/
5013
5014
5015
	// Renders an emphasis on the given date range. `start` is an inclusive, `end` is exclusive.
5016
	renderHighlight: function(start, end) {
5017
		var segs = this.rangeToSegs(start, end);
5018
		var highlightNodes = [];
5019
		var i, seg;
5020
		var el;
5021
5022
		// build an event skeleton for each row that needs it
5023
		for (i = 0; i < segs.length; i++) {
5024
			seg = segs[i];
5025
			el = $(
5026
				this.highlightSkeletonHtml(seg.leftCol, seg.rightCol + 1) // make end exclusive
5027
			);
5028
			el.appendTo(this.rowEls[seg.row]);
5029
			highlightNodes.push(el[0]);
5030
		}
5031
5032
		this.highlightEls = $(highlightNodes); // array -> jQuery set
5033
	},
5034
5035
5036
	// Unrenders any visual emphasis on a date range
5037
	destroyHighlight: function() {
5038
		if (this.highlightEls) {
5039
			this.highlightEls.remove();
5040
			this.highlightEls = null;
5041
		}
5042
	},
5043
5044
5045
	// Generates the HTML used to build a single-row "highlight skeleton", a table that frames highlight cells
5046
	highlightSkeletonHtml: function(startCol, endCol) {
5047
		var colCnt = this.view.colCnt;
5048
		var cellHtml = '';
5049
5050
		if (startCol > 0) {
5051
			cellHtml += '<td colspan="' + startCol + '"/>';
5052
		}
5053
		if (endCol > startCol) {
5054
			cellHtml += '<td colspan="' + (endCol - startCol) + '" class="fc-highlight" />';
5055
		}
5056
		if (colCnt > endCol) {
5057
			cellHtml += '<td colspan="' + (colCnt - endCol) + '"/>';
5058
		}
5059
5060
		cellHtml = this.bookendCells(cellHtml, 'highlight');
5061
5062
		return '' +
5063
			'<div class="fc-highlight-skeleton">' +
5064
				'<table>' +
5065
					'<tr>' +
5066
						cellHtml +
5067
					'</tr>' +
5068
				'</table>' +
5069
			'</div>';
5070
	}
5071
5072
});
5073
5074
;;
5075
5076
/* Event-rendering methods for the DayGrid class
5077
----------------------------------------------------------------------------------------------------------------------*/
5078
5079
$.extend(DayGrid.prototype, {
5080
5081
	segs: null,
5082
	rowStructs: null, // an array of objects, each holding information about a row's event-rendering
5083
5084
5085
	// Render the given events onto the Grid and return the rendered segments
5086
	renderEvents: function(events) {
5087
		var rowStructs = this.rowStructs = this.renderEventRows(events);
5088
		var segs = [];
5089
5090
		// append to each row's content skeleton
5091
		this.rowEls.each(function(i, rowNode) {
5092
			$(rowNode).find('.fc-content-skeleton > table').append(
5093
				rowStructs[i].tbodyEl
5094
			);
5095
			segs.push.apply(segs, rowStructs[i].segs);
5096
		});
5097
5098
		this.segs = segs;
5099
	},
5100
5101
5102
	// Retrieves all segment objects that have been rendered
5103
	getSegs: function() {
5104
		return (this.segs || []).concat(
5105
			this.popoverSegs || [] // segs rendered in the "more" events popover
5106
		);
5107
	},
5108
5109
5110
	// Removes all rendered event elements
5111
	destroyEvents: function() {
5112
		var rowStructs;
5113
		var rowStruct;
5114
5115
		Grid.prototype.destroyEvents.call(this); // call the super-method
5116
5117
		rowStructs = this.rowStructs || [];
5118
		while ((rowStruct = rowStructs.pop())) {
5119
			rowStruct.tbodyEl.remove();
5120
		}
5121
5122
		this.segs = null;
5123
		this.destroySegPopover(); // removes the "more.." events popover
5124
	},
5125
5126
5127
	// Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
5128
	// Returns an array of rowStruct objects (see the bottom of `renderEventRow`).
5129
	renderEventRows: function(events) {
5130
		var segs = this.eventsToSegs(events);
5131
		var rowStructs = [];
5132
		var segRows;
5133
		var row;
5134
5135
		segs = this.renderSegs(segs); // returns a new array with only visible segments
5136
		segRows = this.groupSegRows(segs); // group into nested arrays
5137
5138
		// iterate each row of segment groupings
5139
		for (row = 0; row < segRows.length; row++) {
5140
			rowStructs.push(
5141
				this.renderEventRow(row, segRows[row])
5142
			);
5143
		}
5144
5145
		return rowStructs;
5146
	},
5147
5148
5149
	// Builds the HTML to be used for the default element for an individual segment
5150
	renderSegHtml: function(seg, disableResizing) {
5151
		var view = this.view;
5152
		var isRTL = view.opt('isRTL');
5153
		var event = seg.event;
5154
		var isDraggable = view.isEventDraggable(event);
5155
		var isResizable = !disableResizing && event.allDay && seg.isEnd && view.isEventResizable(event);
5156
		var classes = this.getSegClasses(seg, isDraggable, isResizable);
5157
		var skinCss = this.getEventSkinCss(event);
5158
		var timeHtml = '';
5159
		var titleHtml;
5160
5161
		classes.unshift('fc-day-grid-event');
5162
5163
		// Only display a timed events time if it is the starting segment
5164
		if (!event.allDay && seg.isStart) {
5165
			timeHtml = '<span class="fc-time">' + htmlEscape(view.getEventTimeText(event)) + '</span>';
5166
		}
5167
5168
		titleHtml =
5169
			'<span class="fc-title">' +
5170
				(htmlEscape(event.title || '') || '&nbsp;') + // we always want one line of height
5171
			'</span>';
5172
		
5173
		return '<a class="' + classes.join(' ') + '"' +
5174
				(event.url ?
5175
					' href="' + htmlEscape(event.url) + '"' :
5176
					''
5177
					) +
5178
				(skinCss ?
5179
					' style="' + skinCss + '"' :
5180
					''
5181
					) +
5182
			'>' +
5183
				'<div class="fc-content">' +
5184
					(isRTL ?
5185
						titleHtml + ' ' + timeHtml : // put a natural space in between
5186
						timeHtml + ' ' + titleHtml   //
5187
						) +
5188
				'</div>' +
5189
				(isResizable ?
5190
					'<div class="fc-resizer"/>' :
5191
					''
5192
					) +
5193
			'</a>';
5194
	},
5195
5196
5197
	// Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
5198
	// the segments. Returns object with a bunch of internal data about how the render was calculated.
5199
	renderEventRow: function(row, rowSegs) {
5200
		var view = this.view;
5201
		var colCnt = view.colCnt;
5202
		var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels
5203
		var levelCnt = Math.max(1, segLevels.length); // ensure at least one level
5204
		var tbody = $('<tbody/>');
5205
		var segMatrix = []; // lookup for which segments are rendered into which level+col cells
5206
		var cellMatrix = []; // lookup for all <td> elements of the level+col matrix
5207
		var loneCellMatrix = []; // lookup for <td> elements that only take up a single column
5208
		var i, levelSegs;
5209
		var col;
5210
		var tr;
5211
		var j, seg;
5212
		var td;
5213
5214
		// populates empty cells from the current column (`col`) to `endCol`
5215
		function emptyCellsUntil(endCol) {
5216
			while (col < endCol) {
0 ignored issues
show
Bug introduced by
The variable col is changed as part of the while loop for example by col++ on line 5231. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
5217
				// try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
5218
				td = (loneCellMatrix[i - 1] || [])[col];
5219
				if (td) {
0 ignored issues
show
Bug introduced by
The variable td is changed as part of the while loop for example by loneCellMatrix.i - 1 || [].col on line 5218. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
5220
					td.attr(
5221
						'rowspan',
5222
						parseInt(td.attr('rowspan') || 1, 10) + 1
5223
					);
5224
				}
5225
				else {
5226
					td = $('<td/>');
5227
					tr.append(td);
5228
				}
5229
				cellMatrix[i][col] = td;
5230
				loneCellMatrix[i][col] = td;
5231
				col++;
5232
			}
5233
		}
5234
5235
		for (i = 0; i < levelCnt; i++) { // iterate through all levels
5236
			levelSegs = segLevels[i];
5237
			col = 0;
5238
			tr = $('<tr/>');
5239
5240
			segMatrix.push([]);
5241
			cellMatrix.push([]);
5242
			loneCellMatrix.push([]);
5243
5244
			// levelCnt might be 1 even though there are no actual levels. protect against this.
5245
			// this single empty row is useful for styling.
5246
			if (levelSegs) {
5247
				for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level
5248
					seg = levelSegs[j];
5249
5250
					emptyCellsUntil(seg.leftCol);
5251
5252
					// create a container that occupies or more columns. append the event element.
5253
					td = $('<td class="fc-event-container"/>').append(seg.el);
5254
					if (seg.leftCol != seg.rightCol) {
5255
						td.attr('colspan', seg.rightCol - seg.leftCol + 1);
5256
					}
5257
					else { // a single-column segment
5258
						loneCellMatrix[i][col] = td;
5259
					}
5260
5261
					while (col <= seg.rightCol) {
5262
						cellMatrix[i][col] = td;
5263
						segMatrix[i][col] = seg;
5264
						col++;
5265
					}
5266
5267
					tr.append(td);
5268
				}
5269
			}
5270
5271
			emptyCellsUntil(colCnt); // finish off the row
5272
			this.bookendCells(tr, 'eventSkeleton');
5273
			tbody.append(tr);
5274
		}
5275
5276
		return { // a "rowStruct"
5277
			row: row, // the row number
5278
			tbodyEl: tbody,
5279
			cellMatrix: cellMatrix,
5280
			segMatrix: segMatrix,
5281
			segLevels: segLevels,
5282
			segs: rowSegs
5283
		};
5284
	},
5285
5286
5287
	// Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
5288
	buildSegLevels: function(segs) {
5289
		var levels = [];
5290
		var i, seg;
5291
		var j;
5292
5293
		// Give preference to elements with certain criteria, so they have
5294
		// a chance to be closer to the top.
5295
		segs.sort(compareSegs);
5296
		
5297
		for (i = 0; i < segs.length; i++) {
5298
			seg = segs[i];
5299
5300
			// loop through levels, starting with the topmost, until the segment doesn't collide with other segments
5301
			for (j = 0; j < levels.length; j++) {
5302
				if (!isDaySegCollision(seg, levels[j])) {
5303
					break;
5304
				}
5305
			}
5306
			// `j` now holds the desired subrow index
5307
			seg.level = j;
5308
5309
			// create new level array if needed and append segment
5310
			(levels[j] || (levels[j] = [])).push(seg);
5311
		}
5312
5313
		// order segments left-to-right. very important if calendar is RTL
5314
		for (j = 0; j < levels.length; j++) {
5315
			levels[j].sort(compareDaySegCols);
5316
		}
5317
5318
		return levels;
5319
	},
5320
5321
5322
	// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
5323
	groupSegRows: function(segs) {
5324
		var view = this.view;
5325
		var segRows = [];
5326
		var i;
5327
5328
		for (i = 0; i < view.rowCnt; i++) {
5329
			segRows.push([]);
5330
		}
5331
5332
		for (i = 0; i < segs.length; i++) {
5333
			segRows[segs[i].row].push(segs[i]);
5334
		}
5335
5336
		return segRows;
5337
	}
5338
5339
});
5340
5341
5342
// Computes whether two segments' columns collide. They are assumed to be in the same row.
5343
function isDaySegCollision(seg, otherSegs) {
5344
	var i, otherSeg;
5345
5346
	for (i = 0; i < otherSegs.length; i++) {
5347
		otherSeg = otherSegs[i];
5348
5349
		if (
5350
			otherSeg.leftCol <= seg.rightCol &&
5351
			otherSeg.rightCol >= seg.leftCol
5352
		) {
5353
			return true;
5354
		}
5355
	}
5356
5357
	return false;
5358
}
5359
5360
5361
// A cmp function for determining the leftmost event
5362
function compareDaySegCols(a, b) {
5363
	return a.leftCol - b.leftCol;
5364
}
5365
5366
;;
5367
5368
/* Methods relate to limiting the number events for a given day on a DayGrid
5369
----------------------------------------------------------------------------------------------------------------------*/
5370
5371
$.extend(DayGrid.prototype, {
5372
5373
5374
	segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible
5375
	popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible
5376
5377
5378
	destroySegPopover: function() {
5379
		if (this.segPopover) {
5380
			this.segPopover.hide(); // will trigger destruction of `segPopover` and `popoverSegs`
5381
		}
5382
	},
5383
5384
5385
	// Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
5386
	// `levelLimit` can be false (don't limit), a number, or true (should be computed).
5387
	limitRows: function(levelLimit) {
5388
		var rowStructs = this.rowStructs || [];
5389
		var row; // row #
5390
		var rowLevelLimit;
5391
5392
		for (row = 0; row < rowStructs.length; row++) {
5393
			this.unlimitRow(row);
5394
5395
			if (!levelLimit) {
5396
				rowLevelLimit = false;
5397
			}
5398
			else if (typeof levelLimit === 'number') {
5399
				rowLevelLimit = levelLimit;
5400
			}
5401
			else {
5402
				rowLevelLimit = this.computeRowLevelLimit(row);
5403
			}
5404
5405
			if (rowLevelLimit !== false) {
5406
				this.limitRow(row, rowLevelLimit);
5407
			}
5408
		}
5409
	},
5410
5411
5412
	// Computes the number of levels a row will accomodate without going outside its bounds.
5413
	// Assumes the row is "rigid" (maintains a constant height regardless of what is inside).
5414
	// `row` is the row number.
5415
	computeRowLevelLimit: function(row) {
5416
		var rowEl = this.rowEls.eq(row); // the containing "fake" row div
5417
		var rowHeight = rowEl.height(); // TODO: cache somehow?
5418
		var trEls = this.rowStructs[row].tbodyEl.children();
5419
		var i, trEl;
5420
5421
		// Reveal one level <tr> at a time and stop when we find one out of bounds
5422
		for (i = 0; i < trEls.length; i++) {
5423
			trEl = trEls.eq(i).removeClass('fc-limited'); // get and reveal
5424
			if (trEl.position().top + trEl.outerHeight() > rowHeight) {
5425
				return i;
5426
			}
5427
		}
5428
5429
		return false; // should not limit at all
5430
	},
5431
5432
5433
	// Limits the given grid row to the maximum number of levels and injects "more" links if necessary.
5434
	// `row` is the row number.
5435
	// `levelLimit` is a number for the maximum (inclusive) number of levels allowed.
5436
	limitRow: function(row, levelLimit) {
5437
		var _this = this;
5438
		var view = this.view;
5439
		var rowStruct = this.rowStructs[row];
5440
		var moreNodes = []; // array of "more" <a> links and <td> DOM nodes
5441
		var col = 0; // col #
5442
		var cell;
5443
		var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right
5444
		var cellMatrix; // a matrix (by level, then column) of all <td> jQuery elements in the row
5445
		var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes
5446
		var i, seg;
5447
		var segsBelow; // array of segment objects below `seg` in the current `col`
5448
		var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies
5449
		var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column)
5450
		var td, rowspan;
5451
		var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell
5452
		var j;
5453
		var moreTd, moreWrap, moreLink;
5454
5455
		// Iterates through empty level cells and places "more" links inside if need be
5456
		function emptyCellsUntil(endCol) { // goes from current `col` to `endCol`
5457
			while (col < endCol) {
0 ignored issues
show
Bug introduced by
The variable col is changed as part of the while loop for example by col++ on line 5467. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
5458
				cell = { row: row, col: col };
5459
				segsBelow = _this.getCellSegs(cell, levelLimit);
0 ignored issues
show
Bug introduced by
The variable cell is changed as part of the while loop for example by {IdentifierNode(row,fals...tifierNode(col,false))} on line 5458. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
5460
				if (segsBelow.length) {
0 ignored issues
show
Bug introduced by
The variable segsBelow is changed as part of the while loop for example by _this.getCellSegs(cell, levelLimit) on line 5459. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
5461
					td = cellMatrix[levelLimit - 1][col];
5462
					moreLink = _this.renderMoreLink(cell, segsBelow);
5463
					moreWrap = $('<div/>').append(moreLink);
0 ignored issues
show
Bug introduced by
The variable moreLink is changed as part of the while loop for example by _this.renderMoreLink(cell, segsBelow) on line 5462. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
5464
					td.append(moreWrap);
0 ignored issues
show
Bug introduced by
The variable td is changed as part of the while loop for example by cellMatrix.levelLimit - 1.col on line 5461. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
Bug introduced by
The variable moreWrap is changed as part of the while loop for example by $("<div/>").append(moreLink) on line 5463. Only the value of the last iteration will be visible in this function if it is called after the loop.
Loading history...
5465
					moreNodes.push(moreWrap[0]);
5466
				}
5467
				col++;
5468
			}
5469
		}
5470
5471
		if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit?
5472
			levelSegs = rowStruct.segLevels[levelLimit - 1];
5473
			cellMatrix = rowStruct.cellMatrix;
5474
5475
			limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit
5476
				.addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array
5477
5478
			// iterate though segments in the last allowable level
5479
			for (i = 0; i < levelSegs.length; i++) {
5480
				seg = levelSegs[i];
5481
				emptyCellsUntil(seg.leftCol); // process empty cells before the segment
5482
5483
				// determine *all* segments below `seg` that occupy the same columns
5484
				colSegsBelow = [];
5485
				totalSegsBelow = 0;
5486
				while (col <= seg.rightCol) {
5487
					cell = { row: row, col: col };
5488
					segsBelow = this.getCellSegs(cell, levelLimit);
5489
					colSegsBelow.push(segsBelow);
5490
					totalSegsBelow += segsBelow.length;
5491
					col++;
5492
				}
5493
5494
				if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links?
5495
					td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell
5496
					rowspan = td.attr('rowspan') || 1;
5497
					segMoreNodes = [];
5498
5499
					// make a replacement <td> for each column the segment occupies. will be one for each colspan
5500
					for (j = 0; j < colSegsBelow.length; j++) {
5501
						moreTd = $('<td class="fc-more-cell"/>').attr('rowspan', rowspan);
5502
						segsBelow = colSegsBelow[j];
5503
						cell = { row: row, col: seg.leftCol + j };
5504
						moreLink = this.renderMoreLink(cell, [ seg ].concat(segsBelow)); // count seg as hidden too
5505
						moreWrap = $('<div/>').append(moreLink);
5506
						moreTd.append(moreWrap);
5507
						segMoreNodes.push(moreTd[0]);
5508
						moreNodes.push(moreTd[0]);
5509
					}
5510
5511
					td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements
5512
					limitedNodes.push(td[0]);
5513
				}
5514
			}
5515
5516
			emptyCellsUntil(view.colCnt); // finish off the level
5517
			rowStruct.moreEls = $(moreNodes); // for easy undoing later
5518
			rowStruct.limitedEls = $(limitedNodes); // for easy undoing later
5519
		}
5520
	},
5521
5522
5523
	// Reveals all levels and removes all "more"-related elements for a grid's row.
5524
	// `row` is a row number.
5525
	unlimitRow: function(row) {
5526
		var rowStruct = this.rowStructs[row];
5527
5528
		if (rowStruct.moreEls) {
5529
			rowStruct.moreEls.remove();
5530
			rowStruct.moreEls = null;
5531
		}
5532
5533
		if (rowStruct.limitedEls) {
5534
			rowStruct.limitedEls.removeClass('fc-limited');
5535
			rowStruct.limitedEls = null;
5536
		}
5537
	},
5538
5539
5540
	// Renders an <a> element that represents hidden event element for a cell.
5541
	// Responsible for attaching click handler as well.
5542
	renderMoreLink: function(cell, hiddenSegs) {
5543
		var _this = this;
5544
		var view = this.view;
5545
5546
		return $('<a class="fc-more"/>')
5547
			.text(
5548
				this.getMoreLinkText(hiddenSegs.length)
5549
			)
5550
			.on('click', function(ev) {
5551
				var clickOption = view.opt('eventLimitClick');
5552
				var date = view.cellToDate(cell);
5553
				var moreEl = $(this);
5554
				var dayEl = _this.getCellDayEl(cell);
5555
				var allSegs = _this.getCellSegs(cell);
5556
5557
				// rescope the segments to be within the cell's date
5558
				var reslicedAllSegs = _this.resliceDaySegs(allSegs, date);
5559
				var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date);
5560
5561
				if (typeof clickOption === 'function') {
5562
					// the returned value can be an atomic option
5563
					clickOption = view.trigger('eventLimitClick', null, {
5564
						date: date,
5565
						dayEl: dayEl,
5566
						moreEl: moreEl,
5567
						segs: reslicedAllSegs,
5568
						hiddenSegs: reslicedHiddenSegs
5569
					}, ev);
5570
				}
5571
5572
				if (clickOption === 'popover') {
5573
					_this.showSegPopover(date, cell, moreEl, reslicedAllSegs);
5574
				}
5575
				else if (typeof clickOption === 'string') { // a view name
5576
					view.calendar.zoomTo(date, clickOption);
5577
				}
5578
			});
5579
	},
5580
5581
5582
	// Reveals the popover that displays all events within a cell
5583
	showSegPopover: function(date, cell, moreLink, segs) {
5584
		var _this = this;
5585
		var view = this.view;
5586
		var moreWrap = moreLink.parent(); // the <div> wrapper around the <a>
5587
		var topEl; // the element we want to match the top coordinate of
5588
		var options;
5589
5590
		if (view.rowCnt == 1) {
5591
			topEl = this.view.el; // will cause the popover to cover any sort of header
5592
		}
5593
		else {
5594
			topEl = this.rowEls.eq(cell.row); // will align with top of row
5595
		}
5596
5597
		options = {
5598
			className: 'fc-more-popover',
5599
			content: this.renderSegPopoverContent(date, segs),
5600
			parentEl: this.el,
5601
			top: topEl.offset().top,
5602
			autoHide: true, // when the user clicks elsewhere, hide the popover
5603
			viewportConstrain: view.opt('popoverViewportConstrain'),
5604
			hide: function() {
5605
				// destroy everything when the popover is hidden
5606
				_this.segPopover.destroy();
5607
				_this.segPopover = null;
5608
				_this.popoverSegs = null;
5609
			}
5610
		};
5611
5612
		// Determine horizontal coordinate.
5613
		// We use the moreWrap instead of the <td> to avoid border confusion.
5614
		if (view.opt('isRTL')) {
5615
			options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border
5616
		}
5617
		else {
5618
			options.left = moreWrap.offset().left - 1; // -1 to be over cell border
5619
		}
5620
5621
		this.segPopover = new Popover(options);
5622
		this.segPopover.show();
5623
	},
5624
5625
5626
	// Builds the inner DOM contents of the segment popover
5627
	renderSegPopoverContent: function(date, segs) {
5628
		var view = this.view;
5629
		var isTheme = view.opt('theme');
5630
		var title = date.format(view.opt('dayPopoverFormat'));
5631
		var content = $(
5632
			'<div class="fc-header ' + view.widgetHeaderClass + '">' +
5633
				'<span class="fc-close ' +
5634
					(isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') +
5635
				'"></span>' +
5636
				'<span class="fc-title">' +
5637
					htmlEscape(title) +
5638
				'</span>' +
5639
				'<div class="fc-clear"/>' +
5640
			'</div>' +
5641
			'<div class="fc-body ' + view.widgetContentClass + '">' +
5642
				'<div class="fc-event-container"></div>' +
5643
			'</div>'
5644
		);
5645
		var segContainer = content.find('.fc-event-container');
5646
		var i;
5647
5648
		// render each seg's `el` and only return the visible segs
5649
		segs = this.renderSegs(segs, true); // disableResizing=true
5650
		this.popoverSegs = segs;
5651
5652
		for (i = 0; i < segs.length; i++) {
5653
5654
			// because segments in the popover are not part of a grid coordinate system, provide a hint to any
5655
			// grids that want to do drag-n-drop about which cell it came from
5656
			segs[i].cellDate = date;
5657
5658
			segContainer.append(segs[i].el);
5659
		}
5660
5661
		return content;
5662
	},
5663
5664
5665
	// Given the events within an array of segment objects, reslice them to be in a single day
5666
	resliceDaySegs: function(segs, dayDate) {
5667
		var events = $.map(segs, function(seg) {
5668
			return seg.event;
5669
		});
5670
		var dayStart = dayDate.clone().stripTime();
5671
		var dayEnd = dayStart.clone().add(1, 'days');
5672
5673
		return this.eventsToSegs(events, dayStart, dayEnd);
5674
	},
5675
5676
5677
	// Generates the text that should be inside a "more" link, given the number of events it represents
5678
	getMoreLinkText: function(num) {
5679
		var view = this.view;
5680
		var opt = view.opt('eventLimitText');
5681
5682
		if (typeof opt === 'function') {
5683
			return opt(num);
5684
		}
5685
		else {
5686
			return '+' + num + ' ' + opt;
5687
		}
5688
	},
5689
5690
5691
	// Returns segments within a given cell.
5692
	// If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
5693
	getCellSegs: function(cell, startLevel) {
5694
		var segMatrix = this.rowStructs[cell.row].segMatrix;
5695
		var level = startLevel || 0;
5696
		var segs = [];
5697
		var seg;
5698
5699
		while (level < segMatrix.length) {
5700
			seg = segMatrix[level][cell.col];
5701
			if (seg) {
5702
				segs.push(seg);
5703
			}
5704
			level++;
5705
		}
5706
5707
		return segs;
5708
	}
5709
5710
});
5711
5712
;;
5713
5714
/* A component that renders one or more columns of vertical time slots
5715
----------------------------------------------------------------------------------------------------------------------*/
5716
5717
function TimeGrid(view) {
5718
	Grid.call(this, view); // call the super-constructor
5719
}
5720
5721
5722
TimeGrid.prototype = createObject(Grid.prototype); // define the super-class
5723
$.extend(TimeGrid.prototype, {
5724
5725
	slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines
5726
	snapDuration: null, // granularity of time for dragging and selecting
5727
5728
	minTime: null, // Duration object that denotes the first visible time of any given day
5729
	maxTime: null, // Duration object that denotes the exclusive visible end time of any given day
5730
5731
	dayEls: null, // cells elements in the day-row background
5732
	slatEls: null, // elements running horizontally across all columns
5733
5734
	slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot
5735
5736
	highlightEl: null, // cell skeleton element for rendering the highlight
5737
	helperEl: null, // cell skeleton element for rendering the mock event "helper"
5738
5739
5740
	// Renders the time grid into `this.el`, which should already be assigned.
5741
	// Relies on the view's colCnt. In the future, this component should probably be self-sufficient.
5742
	render: function() {
5743
		this.processOptions();
5744
5745
		this.el.html(this.renderHtml());
5746
5747
		this.dayEls = this.el.find('.fc-day');
5748
		this.slatEls = this.el.find('.fc-slats tr');
5749
5750
		this.computeSlatTops();
5751
5752
		Grid.prototype.render.call(this); // call the super-method
5753
	},
5754
5755
5756
	// Renders the basic HTML skeleton for the grid
5757
	renderHtml: function() {
5758
		return '' +
5759
			'<div class="fc-bg">' +
5760
				'<table>' +
5761
					this.rowHtml('slotBg') + // leverages RowRenderer, which will call slotBgCellHtml
5762
				'</table>' +
5763
			'</div>' +
5764
			'<div class="fc-slats">' +
5765
				'<table>' +
5766
					this.slatRowHtml() +
5767
				'</table>' +
5768
			'</div>';
5769
	},
5770
5771
5772
	// Renders the HTML for a vertical background cell behind the slots.
5773
	// This method is distinct from 'bg' because we wanted a new `rowType` so the View could customize the rendering.
5774
	slotBgCellHtml: function(row, col, date) {
5775
		return this.bgCellHtml(row, col, date);
5776
	},
5777
5778
5779
	// Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
5780
	slatRowHtml: function() {
5781
		var view = this.view;
5782
		var calendar = view.calendar;
5783
		var isRTL = view.opt('isRTL');
5784
		var html = '';
5785
		var slotNormal = this.slotDuration.asMinutes() % 15 === 0;
5786
		var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations
5787
		var slotDate; // will be on the view's first day, but we only care about its time
5788
		var minutes;
5789
		var axisHtml;
5790
5791
		// Calculate the time for each slot
5792
		while (slotTime < this.maxTime) {
5793
			slotDate = view.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues
5794
			minutes = slotDate.minutes();
5795
5796
			axisHtml =
5797
				'<td class="fc-axis fc-time ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' +
5798
					((!slotNormal || !minutes) ? // if irregular slot duration, or on the hour, then display the time
5799
						'<span>' + // for matchCellWidths
5800
							htmlEscape(calendar.formatDate(slotDate, view.opt('axisFormat'))) +
5801
						'</span>' :
5802
						''
5803
						) +
5804
				'</td>';
5805
5806
			html +=
5807
				'<tr ' + (!minutes ? '' : 'class="fc-minor"') + '>' +
5808
					(!isRTL ? axisHtml : '') +
5809
					'<td class="' + view.widgetContentClass + '"/>' +
5810
					(isRTL ? axisHtml : '') +
5811
				"</tr>";
5812
5813
			slotTime.add(this.slotDuration);
5814
		}
5815
5816
		return html;
5817
	},
5818
5819
5820
	// Parses various options into properties of this object
5821
	processOptions: function() {
5822
		var view = this.view;
5823
		var slotDuration = view.opt('slotDuration');
5824
		var snapDuration = view.opt('snapDuration');
5825
5826
		slotDuration = moment.duration(slotDuration);
5827
		snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration;
5828
5829
		this.slotDuration = slotDuration;
5830
		this.snapDuration = snapDuration;
5831
		this.cellDuration = snapDuration; // important to assign this for Grid.events.js
5832
5833
		this.minTime = moment.duration(view.opt('minTime'));
5834
		this.maxTime = moment.duration(view.opt('maxTime'));
5835
	},
5836
5837
5838
	// Slices up a date range into a segment for each column
5839
	rangeToSegs: function(rangeStart, rangeEnd) {
5840
		var view = this.view;
5841
		var segs = [];
5842
		var seg;
5843
		var col;
5844
		var cellDate;
5845
		var colStart, colEnd;
5846
5847
		// normalize
5848
		rangeStart = rangeStart.clone().stripZone();
5849
		rangeEnd = rangeEnd.clone().stripZone();
5850
5851
		for (col = 0; col < view.colCnt; col++) {
5852
			cellDate = view.cellToDate(0, col); // use the View's cell system for this
5853
			colStart = cellDate.clone().time(this.minTime);
5854
			colEnd = cellDate.clone().time(this.maxTime);
5855
			seg = intersectionToSeg(rangeStart, rangeEnd, colStart, colEnd);
5856
			if (seg) {
5857
				seg.col = col;
5858
				segs.push(seg);
5859
			}
5860
		}
5861
5862
		return segs;
5863
	},
5864
5865
5866
	/* Coordinates
5867
	------------------------------------------------------------------------------------------------------------------*/
5868
5869
5870
	// Called when there is a window resize/zoom and we need to recalculate coordinates for the grid
5871
	resize: function() {
5872
		this.computeSlatTops();
5873
		this.updateSegVerticals();
5874
	},
5875
5876
5877
	// Populates the given empty `rows` and `cols` arrays with offset positions of the "snap" cells.
5878
	// "Snap" cells are different the slots because they might have finer granularity.
5879
	buildCoords: function(rows, cols) {
5880
		var colCnt = this.view.colCnt;
5881
		var originTop = this.el.offset().top;
5882
		var snapTime = moment.duration(+this.minTime);
5883
		var p = null;
5884
		var e, n;
5885
5886
		this.dayEls.slice(0, colCnt).each(function(i, _e) {
5887
			e = $(_e);
5888
			n = e.offset().left;
5889
			if (p) {
5890
				p[1] = n;
5891
			}
5892
			p = [ n ];
5893
			cols[i] = p;
5894
		});
5895
		p[1] = n + e.outerWidth();
5896
5897
		p = null;
5898
		while (snapTime < this.maxTime) {
5899
			n = originTop + this.computeTimeTop(snapTime);
5900
			if (p) {
5901
				p[1] = n;
5902
			}
5903
			p = [ n ];
5904
			rows.push(p);
5905
			snapTime.add(this.snapDuration);
5906
		}
5907
		p[1] = originTop + this.computeTimeTop(snapTime); // the position of the exclusive end
5908
	},
5909
5910
5911
	// Gets the datetime for the given slot cell
5912
	getCellDate: function(cell) {
5913
		var view = this.view;
5914
		var calendar = view.calendar;
5915
5916
		return calendar.rezoneDate( // since we are adding a time, it needs to be in the calendar's timezone
5917
			view.cellToDate(0, cell.col) // View's coord system only accounts for start-of-day for column
5918
				.time(this.minTime + this.snapDuration * cell.row)
5919
		);
5920
	},
5921
5922
5923
	// Gets the element that represents the whole-day the cell resides on
5924
	getCellDayEl: function(cell) {
5925
		return this.dayEls.eq(cell.col);
5926
	},
5927
5928
5929
	// Computes the top coordinate, relative to the bounds of the grid, of the given date.
5930
	// A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
5931
	computeDateTop: function(date, startOfDayDate) {
5932
		return this.computeTimeTop(
5933
			moment.duration(
5934
				date.clone().stripZone() - startOfDayDate.clone().stripTime()
5935
			)
5936
		);
5937
	},
5938
5939
5940
	// Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
5941
	computeTimeTop: function(time) {
5942
		var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered
5943
		var slatIndex;
5944
		var slatRemainder;
5945
		var slatTop;
5946
		var slatBottom;
5947
5948
		// constrain. because minTime/maxTime might be customized
5949
		slatCoverage = Math.max(0, slatCoverage);
5950
		slatCoverage = Math.min(this.slatEls.length, slatCoverage);
5951
5952
		slatIndex = Math.floor(slatCoverage); // an integer index of the furthest whole slot
5953
		slatRemainder = slatCoverage - slatIndex;
5954
		slatTop = this.slatTops[slatIndex]; // the top position of the furthest whole slot
5955
5956
		if (slatRemainder) { // time spans part-way into the slot
5957
			slatBottom = this.slatTops[slatIndex + 1];
5958
			return slatTop + (slatBottom - slatTop) * slatRemainder; // part-way between slots
5959
		}
5960
		else {
5961
			return slatTop;
5962
		}
5963
	},
5964
5965
5966
	// Queries each `slatEl` for its position relative to the grid's container and stores it in `slatTops`.
5967
	// Includes the the bottom of the last slat as the last item in the array.
5968
	computeSlatTops: function() {
5969
		var tops = [];
5970
		var top;
5971
5972
		this.slatEls.each(function(i, node) {
5973
			top = $(node).position().top;
5974
			tops.push(top);
5975
		});
5976
5977
		tops.push(top + this.slatEls.last().outerHeight()); // bottom of the last slat
5978
5979
		this.slatTops = tops;
5980
	},
5981
5982
5983
	/* Event Drag Visualization
5984
	------------------------------------------------------------------------------------------------------------------*/
5985
5986
5987
	// Renders a visual indication of an event being dragged over the specified date(s).
5988
	// `end` and `seg` can be null. See View's documentation on renderDrag for more info.
5989
	renderDrag: function(start, end, seg) {
5990
		var opacity;
5991
5992
		if (seg) { // if there is event information for this drag, render a helper event
5993
			this.renderRangeHelper(start, end, seg);
5994
5995
			opacity = this.view.opt('dragOpacity');
5996
			if (opacity !== undefined) {
5997
				this.helperEl.css('opacity', opacity);
5998
			}
5999
6000
			return true; // signal that a helper has been rendered
6001
		}
6002
		else {
6003
			// otherwise, just render a highlight
6004
			this.renderHighlight(
6005
				start,
6006
				end || this.view.calendar.getDefaultEventEnd(false, start)
6007
			);
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
6008
		}
6009
	},
6010
6011
6012
	// Unrenders any visual indication of an event being dragged
6013
	destroyDrag: function() {
6014
		this.destroyHelper();
6015
		this.destroyHighlight();
6016
	},
6017
6018
6019
	/* Event Resize Visualization
6020
	------------------------------------------------------------------------------------------------------------------*/
6021
6022
6023
	// Renders a visual indication of an event being resized
6024
	renderResize: function(start, end, seg) {
6025
		this.renderRangeHelper(start, end, seg);
6026
	},
6027
6028
6029
	// Unrenders any visual indication of an event being resized
6030
	destroyResize: function() {
6031
		this.destroyHelper();
6032
	},
6033
6034
6035
	/* Event Helper
6036
	------------------------------------------------------------------------------------------------------------------*/
6037
6038
6039
	// Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
6040
	renderHelper: function(event, sourceSeg) {
6041
		var res = this.renderEventTable([ event ]);
6042
		var tableEl = res.tableEl;
6043
		var segs = res.segs;
6044
		var i, seg;
6045
		var sourceEl;
6046
6047
		// Try to make the segment that is in the same row as sourceSeg look the same
6048
		for (i = 0; i < segs.length; i++) {
6049
			seg = segs[i];
6050
			if (sourceSeg && sourceSeg.col === seg.col) {
6051
				sourceEl = sourceSeg.el;
6052
				seg.el.css({
6053
					left: sourceEl.css('left'),
6054
					right: sourceEl.css('right'),
6055
					'margin-left': sourceEl.css('margin-left'),
6056
					'margin-right': sourceEl.css('margin-right')
6057
				});
6058
			}
6059
		}
6060
6061
		this.helperEl = $('<div class="fc-helper-skeleton"/>')
6062
			.append(tableEl)
6063
				.appendTo(this.el);
6064
	},
6065
6066
6067
	// Unrenders any mock helper event
6068
	destroyHelper: function() {
6069
		if (this.helperEl) {
6070
			this.helperEl.remove();
6071
			this.helperEl = null;
6072
		}
6073
	},
6074
6075
6076
	/* Selection
6077
	------------------------------------------------------------------------------------------------------------------*/
6078
6079
6080
	// Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.
6081
	renderSelection: function(start, end) {
6082
		if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered
6083
			this.renderRangeHelper(start, end);
6084
		}
6085
		else {
6086
			this.renderHighlight(start, end);
6087
		}
6088
	},
6089
6090
6091
	// Unrenders any visual indication of a selection
6092
	destroySelection: function() {
6093
		this.destroyHelper();
6094
		this.destroyHighlight();
6095
	},
6096
6097
6098
	/* Highlight
6099
	------------------------------------------------------------------------------------------------------------------*/
6100
6101
6102
	// Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive.
6103
	renderHighlight: function(start, end) {
6104
		this.highlightEl = $(
6105
			this.highlightSkeletonHtml(start, end)
6106
		).appendTo(this.el);
6107
	},
6108
6109
6110
	// Unrenders the emphasis on a date range
6111
	destroyHighlight: function() {
6112
		if (this.highlightEl) {
6113
			this.highlightEl.remove();
6114
			this.highlightEl = null;
6115
		}
6116
	},
6117
6118
6119
	// Generates HTML for a table element with containers in each column, responsible for absolutely positioning the
6120
	// highlight elements to cover the highlighted slots.
6121
	highlightSkeletonHtml: function(start, end) {
6122
		var view = this.view;
6123
		var segs = this.rangeToSegs(start, end);
6124
		var cellHtml = '';
6125
		var col = 0;
6126
		var i, seg;
6127
		var dayDate;
6128
		var top, bottom;
6129
6130
		for (i = 0; i < segs.length; i++) { // loop through the segments. one per column
6131
			seg = segs[i];
6132
6133
			// need empty cells beforehand?
6134
			if (col < seg.col) {
6135
				cellHtml += '<td colspan="' + (seg.col - col) + '"/>';
6136
				col = seg.col;
6137
			}
6138
6139
			// compute vertical position
6140
			dayDate = view.cellToDate(0, col);
6141
			top = this.computeDateTop(seg.start, dayDate);
6142
			bottom = this.computeDateTop(seg.end, dayDate); // the y position of the bottom edge
6143
6144
			// generate the cell HTML. bottom becomes negative because it needs to be a CSS value relative to the
6145
			// bottom edge of the zero-height container.
6146
			cellHtml +=
6147
				'<td>' +
6148
					'<div class="fc-highlight-container">' +
6149
						'<div class="fc-highlight" style="top:' + top + 'px;bottom:-' + bottom + 'px"/>' +
6150
					'</div>' +
6151
				'</td>';
6152
6153
			col++;
6154
		}
6155
6156
		// need empty cells after the last segment?
6157
		if (col < view.colCnt) {
6158
			cellHtml += '<td colspan="' + (view.colCnt - col) + '"/>';
6159
		}
6160
6161
		cellHtml = this.bookendCells(cellHtml, 'highlight');
6162
6163
		return '' +
6164
			'<div class="fc-highlight-skeleton">' +
6165
				'<table>' +
6166
					'<tr>' +
6167
						cellHtml +
6168
					'</tr>' +
6169
				'</table>' +
6170
			'</div>';
6171
	}
6172
6173
});
6174
6175
;;
6176
6177
/* Event-rendering methods for the TimeGrid class
6178
----------------------------------------------------------------------------------------------------------------------*/
6179
6180
$.extend(TimeGrid.prototype, {
6181
6182
	segs: null, // segment objects rendered in the component. null of events haven't been rendered yet
6183
	eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements
6184
6185
6186
	// Renders the events onto the grid and returns an array of segments that have been rendered
6187
	renderEvents: function(events) {
6188
		var res = this.renderEventTable(events);
6189
6190
		this.eventSkeletonEl = $('<div class="fc-content-skeleton"/>').append(res.tableEl);
6191
		this.el.append(this.eventSkeletonEl);
6192
6193
		this.segs = res.segs;
6194
	},
6195
6196
6197
	// Retrieves rendered segment objects
6198
	getSegs: function() {
6199
		return this.segs || [];
6200
	},
6201
6202
6203
	// Removes all event segment elements from the view
6204
	destroyEvents: function() {
6205
		Grid.prototype.destroyEvents.call(this); // call the super-method
6206
6207
		if (this.eventSkeletonEl) {
6208
			this.eventSkeletonEl.remove();
6209
			this.eventSkeletonEl = null;
6210
		}
6211
6212
		this.segs = null;
6213
	},
6214
6215
6216
	// Renders and returns the <table> portion of the event-skeleton.
6217
	// Returns an object with properties 'tbodyEl' and 'segs'.
6218
	renderEventTable: function(events) {
6219
		var tableEl = $('<table><tr/></table>');
6220
		var trEl = tableEl.find('tr');
6221
		var segs = this.eventsToSegs(events);
6222
		var segCols;
6223
		var i, seg;
6224
		var col, colSegs;
6225
		var containerEl;
6226
6227
		segs = this.renderSegs(segs); // returns only the visible segs
6228
		segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg
6229
6230
		this.computeSegVerticals(segs); // compute and assign top/bottom
6231
6232
		for (col = 0; col < segCols.length; col++) { // iterate each column grouping
6233
			colSegs = segCols[col];
6234
			placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array
6235
6236
			containerEl = $('<div class="fc-event-container"/>');
6237
6238
			// assign positioning CSS and insert into container
6239
			for (i = 0; i < colSegs.length; i++) {
6240
				seg = colSegs[i];
6241
				seg.el.css(this.generateSegPositionCss(seg));
6242
6243
				// if the height is short, add a className for alternate styling
6244
				if (seg.bottom - seg.top < 30) {
6245
					seg.el.addClass('fc-short');
6246
				}
6247
6248
				containerEl.append(seg.el);
6249
			}
6250
6251
			trEl.append($('<td/>').append(containerEl));
6252
		}
6253
6254
		this.bookendCells(trEl, 'eventSkeleton');
6255
6256
		return  {
6257
			tableEl: tableEl,
6258
			segs: segs
6259
		};
6260
	},
6261
6262
6263
	// Refreshes the CSS top/bottom coordinates for each segment element. Probably after a window resize/zoom.
6264
	updateSegVerticals: function() {
6265
		var segs = this.segs;
6266
		var i;
6267
6268
		if (segs) {
6269
			this.computeSegVerticals(segs);
6270
6271
			for (i = 0; i < segs.length; i++) {
6272
				segs[i].el.css(
6273
					this.generateSegVerticalCss(segs[i])
6274
				);
6275
			}
6276
		}
6277
	},
6278
6279
6280
	// For each segment in an array, computes and assigns its top and bottom properties
6281
	computeSegVerticals: function(segs) {
6282
		var i, seg;
6283
6284
		for (i = 0; i < segs.length; i++) {
6285
			seg = segs[i];
6286
			seg.top = this.computeDateTop(seg.start, seg.start);
6287
			seg.bottom = this.computeDateTop(seg.end, seg.start);
6288
		}
6289
	},
6290
6291
6292
	// Renders the HTML for a single event segment's default rendering
6293
	renderSegHtml: function(seg, disableResizing) {
6294
		var view = this.view;
6295
		var event = seg.event;
6296
		var isDraggable = view.isEventDraggable(event);
6297
		var isResizable = !disableResizing && seg.isEnd && view.isEventResizable(event);
6298
		var classes = this.getSegClasses(seg, isDraggable, isResizable);
6299
		var skinCss = this.getEventSkinCss(event);
6300
		var timeText;
6301
		var fullTimeText; // more verbose time text. for the print stylesheet
6302
		var startTimeText; // just the start time text
6303
6304
		classes.unshift('fc-time-grid-event');
6305
6306
		if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day...
6307
			// Don't display time text on segments that run entirely through a day.
6308
			// That would appear as midnight-midnight and would look dumb.
6309
			// Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
6310
			if (seg.isStart || seg.isEnd) {
6311
				timeText = view.getEventTimeText(seg.start, seg.end);
6312
				fullTimeText = view.getEventTimeText(seg.start, seg.end, 'LT');
6313
				startTimeText = view.getEventTimeText(seg.start, null);
6314
			}
6315
		} else {
6316
			// Display the normal time text for the *event's* times
6317
			timeText = view.getEventTimeText(event);
6318
			fullTimeText = view.getEventTimeText(event, 'LT');
6319
			startTimeText = view.getEventTimeText(event.start, null);
6320
		}
6321
6322
		return '<a class="' + classes.join(' ') + '"' +
6323
			(event.url ?
6324
				' href="' + htmlEscape(event.url) + '"' :
6325
				''
6326
				) +
6327
			(skinCss ?
6328
				' style="' + skinCss + '"' :
6329
				''
6330
				) +
6331
			'>' +
6332
				'<div class="fc-content">' +
6333
					(timeText ?
6334
						'<div class="fc-time"' +
6335
						' data-start="' + htmlEscape(startTimeText) + '"' +
0 ignored issues
show
Bug introduced by
The variable startTimeText does not seem to be initialized in case seg.isStart || seg.isEnd on line 6310 is false. Are you sure the function htmlEscape handles undefined variables?
Loading history...
6336
						' data-full="' + htmlEscape(fullTimeText) + '"' +
0 ignored issues
show
Bug introduced by
The variable fullTimeText does not seem to be initialized in case seg.isStart || seg.isEnd on line 6310 is false. Are you sure the function htmlEscape handles undefined variables?
Loading history...
6337
						'>' +
6338
							'<span>' + htmlEscape(timeText) + '</span>' +
6339
						'</div>' :
6340
						''
6341
						) +
6342
					(event.title ?
6343
						'<div class="fc-title">' +
6344
							htmlEscape(event.title) +
6345
						'</div>' :
6346
						''
6347
						) +
6348
				'</div>' +
6349
				'<div class="fc-bg"/>' +
6350
				(isResizable ?
6351
					'<div class="fc-resizer"/>' :
6352
					''
6353
					) +
6354
			'</a>';
6355
	},
6356
6357
6358
	// Generates an object with CSS properties/values that should be applied to an event segment element.
6359
	// Contains important positioning-related properties that should be applied to any event element, customized or not.
6360
	generateSegPositionCss: function(seg) {
6361
		var view = this.view;
6362
		var isRTL = view.opt('isRTL');
6363
		var shouldOverlap = view.opt('slotEventOverlap');
6364
		var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point
6365
		var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point
6366
		var props = this.generateSegVerticalCss(seg); // get top/bottom first
6367
		var left; // amount of space from left edge, a fraction of the total width
6368
		var right; // amount of space from right edge, a fraction of the total width
6369
6370
		if (shouldOverlap) {
6371
			// double the width, but don't go beyond the maximum forward coordinate (1.0)
6372
			forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2);
6373
		}
6374
6375
		if (isRTL) {
6376
			left = 1 - forwardCoord;
6377
			right = backwardCoord;
6378
		}
6379
		else {
6380
			left = backwardCoord;
6381
			right = 1 - forwardCoord;
6382
		}
6383
6384
		props.zIndex = seg.level + 1; // convert from 0-base to 1-based
6385
		props.left = left * 100 + '%';
6386
		props.right = right * 100 + '%';
6387
6388
		if (shouldOverlap && seg.forwardPressure) {
6389
			// add padding to the edge so that forward stacked events don't cover the resizer's icon
6390
			props[isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width 
6391
		}
6392
6393
		return props;
6394
	},
6395
6396
6397
	// Generates an object with CSS properties for the top/bottom coordinates of a segment element
6398
	generateSegVerticalCss: function(seg) {
6399
		return {
6400
			top: seg.top,
6401
			bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container
6402
		};
6403
	},
6404
6405
6406
	// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
6407
	groupSegCols: function(segs) {
6408
		var view = this.view;
6409
		var segCols = [];
6410
		var i;
6411
6412
		for (i = 0; i < view.colCnt; i++) {
6413
			segCols.push([]);
6414
		}
6415
6416
		for (i = 0; i < segs.length; i++) {
6417
			segCols[segs[i].col].push(segs[i]);
6418
		}
6419
6420
		return segCols;
6421
	}
6422
6423
});
6424
6425
6426
// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
6427
// Also reorders the given array by date!
6428
function placeSlotSegs(segs) {
6429
	var levels;
6430
	var level0;
6431
	var i;
6432
6433
	segs.sort(compareSegs); // order by date
6434
	levels = buildSlotSegLevels(segs);
6435
	computeForwardSlotSegs(levels);
6436
6437
	if ((level0 = levels[0])) {
6438
6439
		for (i = 0; i < level0.length; i++) {
6440
			computeSlotSegPressures(level0[i]);
6441
		}
6442
6443
		for (i = 0; i < level0.length; i++) {
6444
			computeSlotSegCoords(level0[i], 0, 0);
6445
		}
6446
	}
6447
}
6448
6449
6450
// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is
6451
// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date.
6452
function buildSlotSegLevels(segs) {
6453
	var levels = [];
6454
	var i, seg;
6455
	var j;
6456
6457
	for (i=0; i<segs.length; i++) {
6458
		seg = segs[i];
6459
6460
		// go through all the levels and stop on the first level where there are no collisions
6461
		for (j=0; j<levels.length; j++) {
6462
			if (!computeSlotSegCollisions(seg, levels[j]).length) {
6463
				break;
6464
			}
6465
		}
6466
6467
		seg.level = j;
6468
6469
		(levels[j] || (levels[j] = [])).push(seg);
6470
	}
6471
6472
	return levels;
6473
}
6474
6475
6476
// For every segment, figure out the other segments that are in subsequent
6477
// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
6478
function computeForwardSlotSegs(levels) {
6479
	var i, level;
6480
	var j, seg;
6481
	var k;
6482
6483
	for (i=0; i<levels.length; i++) {
6484
		level = levels[i];
6485
6486
		for (j=0; j<level.length; j++) {
6487
			seg = level[j];
6488
6489
			seg.forwardSegs = [];
6490
			for (k=i+1; k<levels.length; k++) {
6491
				computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
6492
			}
6493
		}
6494
	}
6495
}
6496
6497
6498
// Figure out which path forward (via seg.forwardSegs) results in the longest path until
6499
// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
6500
function computeSlotSegPressures(seg) {
6501
	var forwardSegs = seg.forwardSegs;
6502
	var forwardPressure = 0;
6503
	var i, forwardSeg;
6504
6505
	if (seg.forwardPressure === undefined) { // not already computed
6506
6507
		for (i=0; i<forwardSegs.length; i++) {
6508
			forwardSeg = forwardSegs[i];
6509
6510
			// figure out the child's maximum forward path
6511
			computeSlotSegPressures(forwardSeg);
6512
6513
			// either use the existing maximum, or use the child's forward pressure
6514
			// plus one (for the forwardSeg itself)
6515
			forwardPressure = Math.max(
6516
				forwardPressure,
6517
				1 + forwardSeg.forwardPressure
6518
			);
6519
		}
6520
6521
		seg.forwardPressure = forwardPressure;
6522
	}
6523
}
6524
6525
6526
// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
6527
// from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
6528
// seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
6529
//
6530
// The segment might be part of a "series", which means consecutive segments with the same pressure
6531
// who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
6532
// segments behind this one in the current series, and `seriesBackwardCoord` is the starting
6533
// coordinate of the first segment in the series.
6534
function computeSlotSegCoords(seg, seriesBackwardPressure, seriesBackwardCoord) {
6535
	var forwardSegs = seg.forwardSegs;
6536
	var i;
6537
6538
	if (seg.forwardCoord === undefined) { // not already computed
6539
6540
		if (!forwardSegs.length) {
6541
6542
			// if there are no forward segments, this segment should butt up against the edge
6543
			seg.forwardCoord = 1;
6544
		}
6545
		else {
6546
6547
			// sort highest pressure first
6548
			forwardSegs.sort(compareForwardSlotSegs);
6549
6550
			// this segment's forwardCoord will be calculated from the backwardCoord of the
6551
			// highest-pressure forward segment.
6552
			computeSlotSegCoords(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
6553
			seg.forwardCoord = forwardSegs[0].backwardCoord;
6554
		}
6555
6556
		// calculate the backwardCoord from the forwardCoord. consider the series
6557
		seg.backwardCoord = seg.forwardCoord -
6558
			(seg.forwardCoord - seriesBackwardCoord) / // available width for series
6559
			(seriesBackwardPressure + 1); // # of segments in the series
6560
6561
		// use this segment's coordinates to computed the coordinates of the less-pressurized
6562
		// forward segments
6563
		for (i=0; i<forwardSegs.length; i++) {
6564
			computeSlotSegCoords(forwardSegs[i], 0, seg.forwardCoord);
6565
		}
6566
	}
6567
}
6568
6569
6570
// Find all the segments in `otherSegs` that vertically collide with `seg`.
6571
// Append into an optionally-supplied `results` array and return.
6572
function computeSlotSegCollisions(seg, otherSegs, results) {
6573
	results = results || [];
6574
6575
	for (var i=0; i<otherSegs.length; i++) {
6576
		if (isSlotSegCollision(seg, otherSegs[i])) {
6577
			results.push(otherSegs[i]);
6578
		}
6579
	}
6580
6581
	return results;
6582
}
6583
6584
6585
// Do these segments occupy the same vertical space?
6586
function isSlotSegCollision(seg1, seg2) {
6587
	return seg1.bottom > seg2.top && seg1.top < seg2.bottom;
6588
}
6589
6590
6591
// A cmp function for determining which forward segment to rely on more when computing coordinates.
6592
function compareForwardSlotSegs(seg1, seg2) {
6593
	// put higher-pressure first
6594
	return seg2.forwardPressure - seg1.forwardPressure ||
6595
		// put segments that are closer to initial edge first (and favor ones with no coords yet)
6596
		(seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
6597
		// do normal sorting...
6598
		compareSegs(seg1, seg2);
6599
}
6600
6601
;;
6602
6603
/* An abstract class from which other views inherit from
6604
----------------------------------------------------------------------------------------------------------------------*/
6605
// Newer methods should be written as prototype methods, not in the monster `View` function at the bottom.
6606
6607
View.prototype = {
6608
6609
	calendar: null, // owner Calendar object
6610
	coordMap: null, // a CoordMap object for converting pixel regions to dates
6611
	el: null, // the view's containing element. set by Calendar
6612
6613
	// important Moments
6614
	start: null, // the date of the very first cell
6615
	end: null, // the date after the very last cell
6616
	intervalStart: null, // the start of the interval of time the view represents (1st of month for month view)
6617
	intervalEnd: null, // the exclusive end of the interval of time the view represents
6618
6619
	// used for cell-to-date and date-to-cell calculations
6620
	rowCnt: null, // # of weeks
6621
	colCnt: null, // # of days displayed in a week
6622
6623
	isSelected: false, // boolean whether cells are user-selected or not
6624
6625
	// subclasses can optionally use a scroll container
6626
	scrollerEl: null, // the element that will most likely scroll when content is too tall
6627
	scrollTop: null, // cached vertical scroll value
6628
6629
	// classNames styled by jqui themes
6630
	widgetHeaderClass: null,
6631
	widgetContentClass: null,
6632
	highlightStateClass: null,
6633
6634
	// document handlers, bound to `this` object
6635
	documentMousedownProxy: null,
6636
	documentDragStartProxy: null,
6637
6638
6639
	// Serves as a "constructor" to suppliment the monster `View` constructor below
6640
	init: function() {
6641
		var tm = this.opt('theme') ? 'ui' : 'fc';
6642
6643
		this.widgetHeaderClass = tm + '-widget-header';
6644
		this.widgetContentClass = tm + '-widget-content';
6645
		this.highlightStateClass = tm + '-state-highlight';
6646
6647
		// save references to `this`-bound handlers
6648
		this.documentMousedownProxy = $.proxy(this, 'documentMousedown');
6649
		this.documentDragStartProxy = $.proxy(this, 'documentDragStart');
6650
	},
6651
6652
6653
	// Renders the view inside an already-defined `this.el`.
6654
	// Subclasses should override this and then call the super method afterwards.
6655
	render: function() {
6656
		this.updateSize();
6657
		this.trigger('viewRender', this, this, this.el);
6658
6659
		// attach handlers to document. do it here to allow for destroy/rerender
6660
		$(document)
6661
			.on('mousedown', this.documentMousedownProxy)
6662
			.on('dragstart', this.documentDragStartProxy); // jqui drag
6663
	},
6664
6665
6666
	// Clears all view rendering, event elements, and unregisters handlers
6667
	destroy: function() {
6668
		this.unselect();
6669
		this.trigger('viewDestroy', this, this, this.el);
6670
		this.destroyEvents();
6671
		this.el.empty(); // removes inner contents but leaves the element intact
6672
6673
		$(document)
6674
			.off('mousedown', this.documentMousedownProxy)
6675
			.off('dragstart', this.documentDragStartProxy);
6676
	},
6677
6678
6679
	// Used to determine what happens when the users clicks next/prev. Given -1 for prev, 1 for next.
6680
	// Should apply the delta to `date` (a Moment) and return it.
6681
	incrementDate: function(date, delta) {
6682
		// subclasses should implement
6683
	},
6684
6685
6686
	/* Dimensions
6687
	------------------------------------------------------------------------------------------------------------------*/
6688
6689
6690
	// Refreshes anything dependant upon sizing of the container element of the grid
6691
	updateSize: function(isResize) {
6692
		if (isResize) {
6693
			this.recordScroll();
6694
		}
6695
		this.updateHeight();
6696
		this.updateWidth();
6697
	},
6698
6699
6700
	// Refreshes the horizontal dimensions of the calendar
6701
	updateWidth: function() {
6702
		// subclasses should implement
6703
	},
6704
6705
6706
	// Refreshes the vertical dimensions of the calendar
6707
	updateHeight: function() {
6708
		var calendar = this.calendar; // we poll the calendar for height information
6709
6710
		this.setHeight(
6711
			calendar.getSuggestedViewHeight(),
6712
			calendar.isHeightAuto()
6713
		);
6714
	},
6715
6716
6717
	// Updates the vertical dimensions of the calendar to the specified height.
6718
	// if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.
6719
	setHeight: function(height, isAuto) {
6720
		// subclasses should implement
6721
	},
6722
6723
6724
	// Given the total height of the view, return the number of pixels that should be used for the scroller.
6725
	// Utility for subclasses.
6726
	computeScrollerHeight: function(totalHeight) {
6727
		var both = this.el.add(this.scrollerEl);
6728
		var otherHeight; // cumulative height of everything that is not the scrollerEl in the view (header+borders)
6729
6730
		// fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
6731
		both.css({
6732
			position: 'relative', // cause a reflow, which will force fresh dimension recalculation
6733
			left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
6734
		});
6735
		otherHeight = this.el.outerHeight() - this.scrollerEl.height(); // grab the dimensions
6736
		both.css({ position: '', left: '' }); // undo hack
6737
6738
		return totalHeight - otherHeight;
6739
	},
6740
6741
6742
	// Called for remembering the current scroll value of the scroller.
6743
	// Should be called before there is a destructive operation (like removing DOM elements) that might inadvertently
6744
	// change the scroll of the container.
6745
	recordScroll: function() {
6746
		if (this.scrollerEl) {
6747
			this.scrollTop = this.scrollerEl.scrollTop();
6748
		}
6749
	},
6750
6751
6752
	// Set the scroll value of the scroller to the previously recorded value.
6753
	// Should be called after we know the view's dimensions have been restored following some type of destructive
6754
	// operation (like temporarily removing DOM elements).
6755
	restoreScroll: function() {
6756
		if (this.scrollTop !== null) {
6757
			this.scrollerEl.scrollTop(this.scrollTop);
6758
		}
6759
	},
6760
6761
6762
	/* Events
6763
	------------------------------------------------------------------------------------------------------------------*/
6764
6765
6766
	// Renders the events onto the view.
6767
	// Should be overriden by subclasses. Subclasses should call the super-method afterwards.
6768
	renderEvents: function(events) {
6769
		this.segEach(function(seg) {
6770
			this.trigger('eventAfterRender', seg.event, seg.event, seg.el);
6771
		});
6772
		this.trigger('eventAfterAllRender');
6773
	},
6774
6775
6776
	// Removes event elements from the view.
6777
	// Should be overridden by subclasses. Should call this super-method FIRST, then subclass DOM destruction.
6778
	destroyEvents: function() {
6779
		this.segEach(function(seg) {
6780
			this.trigger('eventDestroy', seg.event, seg.event, seg.el);
6781
		});
6782
	},
6783
6784
6785
	// Given an event and the default element used for rendering, returns the element that should actually be used.
6786
	// Basically runs events and elements through the eventRender hook.
6787
	resolveEventEl: function(event, el) {
6788
		var custom = this.trigger('eventRender', event, event, el);
6789
6790
		if (custom === false) { // means don't render at all
6791
			el = null;
6792
		}
6793
		else if (custom && custom !== true) {
6794
			el = $(custom);
6795
		}
6796
6797
		return el;
6798
	},
6799
6800
6801
	// Hides all rendered event segments linked to the given event
6802
	showEvent: function(event) {
6803
		this.segEach(function(seg) {
6804
			seg.el.css('visibility', '');
6805
		}, event);
6806
	},
6807
6808
6809
	// Shows all rendered event segments linked to the given event
6810
	hideEvent: function(event) {
6811
		this.segEach(function(seg) {
6812
			seg.el.css('visibility', 'hidden');
6813
		}, event);
6814
	},
6815
6816
6817
	// Iterates through event segments. Goes through all by default.
6818
	// If the optional `event` argument is specified, only iterates through segments linked to that event.
6819
	// The `this` value of the callback function will be the view.
6820
	segEach: function(func, event) {
6821
		var segs = this.getSegs();
6822
		var i;
6823
6824
		for (i = 0; i < segs.length; i++) {
6825
			if (!event || segs[i].event._id === event._id) {
6826
				func.call(this, segs[i]);
6827
			}
6828
		}
6829
	},
6830
6831
6832
	// Retrieves all the rendered segment objects for the view
6833
	getSegs: function() {
6834
		// subclasses must implement
6835
	},
6836
6837
6838
	/* Event Drag Visualization
6839
	------------------------------------------------------------------------------------------------------------------*/
6840
6841
6842
	// Renders a visual indication of an event hovering over the specified date.
6843
	// `end` is a Moment and might be null.
6844
	// `seg` might be null. if specified, it is the segment object of the event being dragged.
6845
	//       otherwise, an external event from outside the calendar is being dragged.
6846
	renderDrag: function(start, end, seg) {
6847
		// subclasses should implement
6848
	},
6849
6850
6851
	// Unrenders a visual indication of event hovering
6852
	destroyDrag: function() {
6853
		// subclasses should implement
6854
	},
6855
6856
6857
	// Handler for accepting externally dragged events being dropped in the view.
6858
	// Gets called when jqui's 'dragstart' is fired.
6859
	documentDragStart: function(ev, ui) {
6860
		var _this = this;
6861
		var dropDate = null;
6862
		var dragListener;
6863
6864
		if (this.opt('droppable')) { // only listen if this setting is on
6865
6866
			// listener that tracks mouse movement over date-associated pixel regions
6867
			dragListener = new DragListener(this.coordMap, {
6868
				cellOver: function(cell, date) {
6869
					dropDate = date;
6870
					_this.renderDrag(date);
6871
				},
6872
				cellOut: function() {
6873
					dropDate = null;
6874
					_this.destroyDrag();
6875
				}
6876
			});
6877
6878
			// gets called, only once, when jqui drag is finished
6879
			$(document).one('dragstop', function(ev, ui) {
6880
				_this.destroyDrag();
6881
				if (dropDate) {
6882
					_this.trigger('drop', ev.target, dropDate, ev, ui);
6883
				}
6884
			});
6885
6886
			dragListener.startDrag(ev); // start listening immediately
6887
		}
6888
	},
6889
6890
6891
	/* Selection
6892
	------------------------------------------------------------------------------------------------------------------*/
6893
6894
6895
	// Selects a date range on the view. `start` and `end` are both Moments.
6896
	// `ev` is the native mouse event that begin the interaction.
6897
	select: function(start, end, ev) {
6898
		this.unselect(ev);
6899
		this.renderSelection(start, end);
6900
		this.reportSelection(start, end, ev);
6901
	},
6902
6903
6904
	// Renders a visual indication of the selection
6905
	renderSelection: function(start, end) {
6906
		// subclasses should implement
6907
	},
6908
6909
6910
	// Called when a new selection is made. Updates internal state and triggers handlers.
6911
	reportSelection: function(start, end, ev) {
6912
		this.isSelected = true;
6913
		this.trigger('select', null, start, end, ev);
6914
	},
6915
6916
6917
	// Undoes a selection. updates in the internal state and triggers handlers.
6918
	// `ev` is the native mouse event that began the interaction.
6919
	unselect: function(ev) {
6920
		if (this.isSelected) {
6921
			this.isSelected = false;
6922
			this.destroySelection();
6923
			this.trigger('unselect', null, ev);
6924
		}
6925
	},
6926
6927
6928
	// Unrenders a visual indication of selection
6929
	destroySelection: function() {
6930
		// subclasses should implement
6931
	},
6932
6933
6934
	// Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on
6935
	documentMousedown: function(ev) {
6936
		var ignore;
6937
6938
		// is there a selection, and has the user made a proper left click?
6939
		if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) {
6940
6941
			// only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
6942
			ignore = this.opt('unselectCancel');
6943
			if (!ignore || !$(ev.target).closest(ignore).length) {
6944
				this.unselect(ev);
6945
			}
6946
		}
6947
	}
6948
6949
};
6950
6951
6952
// We are mixing JavaScript OOP design patterns here by putting methods and member variables in the closed scope of the
6953
// constructor. Going forward, methods should be part of the prototype.
6954
function View(calendar) {
6955
	var t = this;
6956
	
6957
	// exports
6958
	t.calendar = calendar;
6959
	t.opt = opt;
6960
	t.trigger = trigger;
6961
	t.isEventDraggable = isEventDraggable;
6962
	t.isEventResizable = isEventResizable;
6963
	t.eventDrop = eventDrop;
6964
	t.eventResize = eventResize;
6965
	
6966
	// imports
6967
	var reportEventChange = calendar.reportEventChange;
6968
	
6969
	// locals
6970
	var options = calendar.options;
6971
	var nextDayThreshold = moment.duration(options.nextDayThreshold);
6972
6973
6974
	t.init(); // the "constructor" that concerns the prototype methods
6975
	
6976
	
6977
	function opt(name) {
6978
		var v = options[name];
6979
		if ($.isPlainObject(v) && !isForcedAtomicOption(name)) {
6980
			return smartProperty(v, t.name);
6981
		}
6982
		return v;
6983
	}
6984
6985
	
6986
	function trigger(name, thisObj) {
6987
		return calendar.trigger.apply(
6988
			calendar,
6989
			[name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t])
6990
		);
6991
	}
6992
	
6993
6994
6995
	/* Event Editable Boolean Calculations
6996
	------------------------------------------------------------------------------*/
6997
6998
	
6999
	function isEventDraggable(event) {
7000
		var source = event.source || {};
7001
7002
		return firstDefined(
7003
			event.startEditable,
7004
			source.startEditable,
7005
			opt('eventStartEditable'),
7006
			event.editable,
7007
			source.editable,
7008
			opt('editable')
7009
		);
7010
	}
7011
	
7012
	
7013
	function isEventResizable(event) {
7014
		var source = event.source || {};
7015
7016
		return firstDefined(
7017
			event.durationEditable,
7018
			source.durationEditable,
7019
			opt('eventDurationEditable'),
7020
			event.editable,
7021
			source.editable,
7022
			opt('editable')
7023
		);
7024
	}
7025
	
7026
	
7027
	
7028
	/* Event Elements
7029
	------------------------------------------------------------------------------*/
7030
7031
7032
	// Compute the text that should be displayed on an event's element.
7033
	// Based off the settings of the view. Possible signatures:
7034
	//   .getEventTimeText(event, formatStr)
7035
	//   .getEventTimeText(startMoment, endMoment, formatStr)
7036
	//   .getEventTimeText(startMoment, null, formatStr)
7037
	// `timeFormat` is used but the `formatStr` argument can be used to override.
7038
	t.getEventTimeText = function(event, formatStr) {
7039
		var start;
7040
		var end;
7041
7042
		if (typeof event === 'object' && typeof formatStr === 'object') {
7043
			// first two arguments are actually moments (or null). shift arguments.
7044
			start = event;
7045
			end = formatStr;
7046
			formatStr = arguments[2];
7047
		}
7048
		else {
7049
			// otherwise, an event object was the first argument
7050
			start = event.start;
7051
			end = event.end;
7052
		}
7053
7054
		formatStr = formatStr || opt('timeFormat');
7055
7056
		if (end && opt('displayEventEnd')) {
7057
			return calendar.formatRange(start, end, formatStr);
7058
		}
7059
		else {
7060
			return calendar.formatDate(start, formatStr);
7061
		}
7062
	};
7063
7064
	
7065
	
7066
	/* Event Modification Reporting
7067
	---------------------------------------------------------------------------------*/
7068
7069
	
7070
	function eventDrop(el, event, newStart, ev) {
7071
		var mutateResult = calendar.mutateEvent(event, newStart, null);
7072
7073
		trigger(
7074
			'eventDrop',
7075
			el,
7076
			event,
7077
			mutateResult.dateDelta,
7078
			function() {
7079
				mutateResult.undo();
7080
				reportEventChange();
7081
			},
7082
			ev,
7083
			{} // jqui dummy
7084
		);
7085
7086
		reportEventChange();
7087
	}
7088
7089
7090
	function eventResize(el, event, newEnd, ev) {
7091
		var mutateResult = calendar.mutateEvent(event, null, newEnd);
7092
7093
		trigger(
7094
			'eventResize',
7095
			el,
7096
			event,
7097
			mutateResult.durationDelta,
7098
			function() {
7099
				mutateResult.undo();
7100
				reportEventChange();
7101
			},
7102
			ev,
7103
			{} // jqui dummy
7104
		);
7105
7106
		reportEventChange();
7107
	}
7108
7109
7110
	// ====================================================================================================
7111
	// Utilities for day "cells"
7112
	// ====================================================================================================
7113
	// The "basic" views are completely made up of day cells.
7114
	// The "agenda" views have day cells at the top "all day" slot.
7115
	// This was the obvious common place to put these utilities, but they should be abstracted out into
7116
	// a more meaningful class (like DayEventRenderer).
7117
	// ====================================================================================================
7118
7119
7120
	// For determining how a given "cell" translates into a "date":
7121
	//
7122
	// 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first).
7123
	//    Keep in mind that column indices are inverted with isRTL. This is taken into account.
7124
	//
7125
	// 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view).
7126
	//
7127
	// 3. Convert the "day offset" into a "date" (a Moment).
7128
	//
7129
	// The reverse transformation happens when transforming a date into a cell.
7130
7131
7132
	// exports
7133
	t.isHiddenDay = isHiddenDay;
7134
	t.skipHiddenDays = skipHiddenDays;
7135
	t.getCellsPerWeek = getCellsPerWeek;
7136
	t.dateToCell = dateToCell;
7137
	t.dateToDayOffset = dateToDayOffset;
7138
	t.dayOffsetToCellOffset = dayOffsetToCellOffset;
7139
	t.cellOffsetToCell = cellOffsetToCell;
7140
	t.cellToDate = cellToDate;
7141
	t.cellToCellOffset = cellToCellOffset;
7142
	t.cellOffsetToDayOffset = cellOffsetToDayOffset;
7143
	t.dayOffsetToDate = dayOffsetToDate;
7144
	t.rangeToSegments = rangeToSegments;
7145
	t.isMultiDayEvent = isMultiDayEvent;
7146
7147
7148
	// internals
7149
	var hiddenDays = opt('hiddenDays') || []; // array of day-of-week indices that are hidden
7150
	var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
7151
	var cellsPerWeek;
7152
	var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week
7153
	var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week
7154
	var isRTL = opt('isRTL');
7155
7156
7157
	// initialize important internal variables
7158
	(function() {
7159
7160
		if (opt('weekends') === false) {
7161
			hiddenDays.push(0, 6); // 0=sunday, 6=saturday
7162
		}
7163
7164
		// Loop through a hypothetical week and determine which
7165
		// days-of-week are hidden. Record in both hashes (one is the reverse of the other).
7166
		for (var dayIndex=0, cellIndex=0; dayIndex<7; dayIndex++) {
7167
			dayToCellMap[dayIndex] = cellIndex;
7168
			isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1;
7169
			if (!isHiddenDayHash[dayIndex]) {
7170
				cellToDayMap[cellIndex] = dayIndex;
7171
				cellIndex++;
7172
			}
7173
		}
7174
7175
		cellsPerWeek = cellIndex;
7176
		if (!cellsPerWeek) {
7177
			throw 'invalid hiddenDays'; // all days were hidden? bad.
7178
		}
7179
7180
	})();
7181
7182
7183
	// Is the current day hidden?
7184
	// `day` is a day-of-week index (0-6), or a Moment
7185
	function isHiddenDay(day) {
7186
		if (moment.isMoment(day)) {
7187
			day = day.day();
7188
		}
7189
		return isHiddenDayHash[day];
7190
	}
7191
7192
7193
	function getCellsPerWeek() {
7194
		return cellsPerWeek;
7195
	}
7196
7197
7198
	// Incrementing the current day until it is no longer a hidden day, returning a copy.
7199
	// If the initial value of `date` is not a hidden day, don't do anything.
7200
	// Pass `isExclusive` as `true` if you are dealing with an end date.
7201
	// `inc` defaults to `1` (increment one day forward each time)
7202
	function skipHiddenDays(date, inc, isExclusive) {
7203
		var out = date.clone();
7204
		inc = inc || 1;
7205
		while (
7206
			isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7]
7207
		) {
7208
			out.add(inc, 'days');
7209
		}
7210
		return out;
7211
	}
7212
7213
7214
	//
7215
	// TRANSFORMATIONS: cell -> cell offset -> day offset -> date
7216
	//
7217
7218
	// cell -> date (combines all transformations)
7219
	// Possible arguments:
7220
	// - row, col
7221
	// - { row:#, col: # }
7222
	function cellToDate() {
7223
		var cellOffset = cellToCellOffset.apply(null, arguments);
7224
		var dayOffset = cellOffsetToDayOffset(cellOffset);
7225
		var date = dayOffsetToDate(dayOffset);
7226
		return date;
7227
	}
7228
7229
	// cell -> cell offset
7230
	// Possible arguments:
7231
	// - row, col
7232
	// - { row:#, col:# }
7233
	function cellToCellOffset(row, col) {
7234
		var colCnt = t.colCnt;
7235
7236
		// rtl variables. wish we could pre-populate these. but where?
7237
		var dis = isRTL ? -1 : 1;
7238
		var dit = isRTL ? colCnt - 1 : 0;
7239
7240
		if (typeof row == 'object') {
7241
			col = row.col;
7242
			row = row.row;
7243
		}
7244
		var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit)
7245
7246
		return cellOffset;
7247
	}
7248
7249
	// cell offset -> day offset
7250
	function cellOffsetToDayOffset(cellOffset) {
7251
		var day0 = t.start.day(); // first date's day of week
7252
		cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week
7253
		return Math.floor(cellOffset / cellsPerWeek) * 7 + // # of days from full weeks
7254
			cellToDayMap[ // # of days from partial last week
7255
				(cellOffset % cellsPerWeek + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets
7256
			] -
7257
			day0; // adjustment for beginning-of-week normalization
7258
	}
7259
7260
	// day offset -> date
7261
	function dayOffsetToDate(dayOffset) {
7262
		return t.start.clone().add(dayOffset, 'days');
7263
	}
7264
7265
7266
	//
7267
	// TRANSFORMATIONS: date -> day offset -> cell offset -> cell
7268
	//
7269
7270
	// date -> cell (combines all transformations)
7271
	function dateToCell(date) {
7272
		var dayOffset = dateToDayOffset(date);
7273
		var cellOffset = dayOffsetToCellOffset(dayOffset);
7274
		var cell = cellOffsetToCell(cellOffset);
7275
		return cell;
7276
	}
7277
7278
	// date -> day offset
7279
	function dateToDayOffset(date) {
7280
		return date.clone().stripTime().diff(t.start, 'days');
7281
	}
7282
7283
	// day offset -> cell offset
7284
	function dayOffsetToCellOffset(dayOffset) {
7285
		var day0 = t.start.day(); // first date's day of week
7286
		dayOffset += day0; // normalize dayOffset to beginning-of-week
7287
		return Math.floor(dayOffset / 7) * cellsPerWeek + // # of cells from full weeks
7288
			dayToCellMap[ // # of cells from partial last week
7289
				(dayOffset % 7 + 7) % 7 // crazy math to handle negative dayOffsets
7290
			] -
7291
			dayToCellMap[day0]; // adjustment for beginning-of-week normalization
7292
	}
7293
7294
	// cell offset -> cell (object with row & col keys)
7295
	function cellOffsetToCell(cellOffset) {
7296
		var colCnt = t.colCnt;
7297
7298
		// rtl variables. wish we could pre-populate these. but where?
7299
		var dis = isRTL ? -1 : 1;
7300
		var dit = isRTL ? colCnt - 1 : 0;
7301
7302
		var row = Math.floor(cellOffset / colCnt);
7303
		var col = ((cellOffset % colCnt + colCnt) % colCnt) * dis + dit; // column, adjusted for RTL (dis & dit)
7304
		return {
7305
			row: row,
7306
			col: col
7307
		};
7308
	}
7309
7310
7311
	//
7312
	// Converts a date range into an array of segment objects.
7313
	// "Segments" are horizontal stretches of time, sliced up by row.
7314
	// A segment object has the following properties:
7315
	// - row
7316
	// - cols
7317
	// - isStart
7318
	// - isEnd
7319
	//
7320
	function rangeToSegments(start, end) {
7321
7322
		var rowCnt = t.rowCnt;
7323
		var colCnt = t.colCnt;
7324
		var segments = []; // array of segments to return
7325
7326
		// day offset for given date range
7327
		var dayRange = computeDayRange(start, end); // convert to a whole-day range
7328
		var rangeDayOffsetStart = dateToDayOffset(dayRange.start);
7329
		var rangeDayOffsetEnd = dateToDayOffset(dayRange.end); // an exclusive value
7330
7331
		// first and last cell offset for the given date range
7332
		// "last" implies inclusivity
7333
		var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart);
7334
		var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1;
7335
7336
		// loop through all the rows in the view
7337
		for (var row=0; row<rowCnt; row++) {
7338
7339
			// first and last cell offset for the row
7340
			var rowCellOffsetFirst = row * colCnt;
7341
			var rowCellOffsetLast = rowCellOffsetFirst + colCnt - 1;
7342
7343
			// get the segment's cell offsets by constraining the range's cell offsets to the bounds of the row
7344
			var segmentCellOffsetFirst = Math.max(rangeCellOffsetFirst, rowCellOffsetFirst);
7345
			var segmentCellOffsetLast = Math.min(rangeCellOffsetLast, rowCellOffsetLast);
7346
7347
			// make sure segment's offsets are valid and in view
7348
			if (segmentCellOffsetFirst <= segmentCellOffsetLast) {
7349
7350
				// translate to cells
7351
				var segmentCellFirst = cellOffsetToCell(segmentCellOffsetFirst);
7352
				var segmentCellLast = cellOffsetToCell(segmentCellOffsetLast);
7353
7354
				// view might be RTL, so order by leftmost column
7355
				var cols = [ segmentCellFirst.col, segmentCellLast.col ].sort();
7356
7357
				// Determine if segment's first/last cell is the beginning/end of the date range.
7358
				// We need to compare "day offset" because "cell offsets" are often ambiguous and
7359
				// can translate to multiple days, and an edge case reveals itself when we the
7360
				// range's first cell is hidden (we don't want isStart to be true).
7361
				var isStart = cellOffsetToDayOffset(segmentCellOffsetFirst) == rangeDayOffsetStart;
7362
				var isEnd = cellOffsetToDayOffset(segmentCellOffsetLast) + 1 == rangeDayOffsetEnd;
7363
				                                                   // +1 for comparing exclusively
7364
7365
				segments.push({
7366
					row: row,
7367
					leftCol: cols[0],
7368
					rightCol: cols[1],
7369
					isStart: isStart,
7370
					isEnd: isEnd
7371
				});
7372
			}
7373
		}
7374
7375
		return segments;
7376
	}
7377
7378
7379
	// Returns the date range of the full days the given range visually appears to occupy.
7380
	// Returns object with properties `start` (moment) and `end` (moment, exclusive end).
7381
	function computeDayRange(start, end) {
7382
		var startDay = start.clone().stripTime(); // the beginning of the day the range starts
7383
		var endDay;
7384
		var endTimeMS;
7385
7386
		if (end) {
7387
			endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends
7388
			endTimeMS = +end.time(); // # of milliseconds into `endDay`
7389
7390
			// If the end time is actually inclusively part of the next day and is equal to or
7391
			// beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
7392
			// Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
7393
			if (endTimeMS && endTimeMS >= nextDayThreshold) {
7394
				endDay.add(1, 'days');
7395
			}
7396
		}
7397
7398
		// If no end was specified, or if it is within `startDay` but not past nextDayThreshold,
7399
		// assign the default duration of one day.
7400
		if (!end || endDay <= startDay) {
0 ignored issues
show
Bug introduced by
The variable endDay does not seem to be initialized in case end on line 7386 is false. Are you sure this can never be the case?
Loading history...
7401
			endDay = startDay.clone().add(1, 'days');
7402
		}
7403
7404
		return { start: startDay, end: endDay };
7405
	}
7406
7407
7408
	// Does the given event visually appear to occupy more than one day?
7409
	function isMultiDayEvent(event) {
7410
		var range = computeDayRange(event.start, event.end);
7411
7412
		return range.end.diff(range.start, 'days') > 1;
7413
	}
7414
7415
}
7416
7417
;;
7418
7419
/* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.
7420
----------------------------------------------------------------------------------------------------------------------*/
7421
// It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.
7422
// It is responsible for managing width/height.
7423
7424
function BasicView(calendar) {
7425
	View.call(this, calendar); // call the super-constructor
7426
	this.dayGrid = new DayGrid(this);
7427
	this.coordMap = this.dayGrid.coordMap; // the view's date-to-cell mapping is identical to the subcomponent's
7428
}
7429
7430
7431
BasicView.prototype = createObject(View.prototype); // define the super-class
7432
$.extend(BasicView.prototype, {
7433
7434
	dayGrid: null, // the main subcomponent that does most of the heavy lifting
7435
7436
	dayNumbersVisible: false, // display day numbers on each day cell?
7437
	weekNumbersVisible: false, // display week numbers along the side?
7438
7439
	weekNumberWidth: null, // width of all the week-number cells running down the side
7440
7441
	headRowEl: null, // the fake row element of the day-of-week header
7442
7443
7444
	// Renders the view into `this.el`, which should already be assigned.
7445
	// rowCnt, colCnt, and dayNumbersVisible have been calculated by a subclass and passed here.
7446
	render: function(rowCnt, colCnt, dayNumbersVisible) {
7447
7448
		// needed for cell-to-date and date-to-cell calculations in View
7449
		this.rowCnt = rowCnt;
7450
		this.colCnt = colCnt;
7451
7452
		this.dayNumbersVisible = dayNumbersVisible;
7453
		this.weekNumbersVisible = this.opt('weekNumbers');
7454
		this.dayGrid.numbersVisible = this.dayNumbersVisible || this.weekNumbersVisible;
7455
7456
		this.el.addClass('fc-basic-view').html(this.renderHtml());
7457
7458
		this.headRowEl = this.el.find('thead .fc-row');
7459
7460
		this.scrollerEl = this.el.find('.fc-day-grid-container');
7461
		this.dayGrid.coordMap.containerEl = this.scrollerEl; // constrain clicks/etc to the dimensions of the scroller
7462
7463
		this.dayGrid.el = this.el.find('.fc-day-grid');
7464
		this.dayGrid.render(this.hasRigidRows());
7465
7466
		View.prototype.render.call(this); // call the super-method
7467
	},
7468
7469
7470
	// Make subcomponents ready for cleanup
7471
	destroy: function() {
7472
		this.dayGrid.destroy();
7473
		View.prototype.destroy.call(this); // call the super-method
7474
	},
7475
7476
7477
	// Builds the HTML skeleton for the view.
7478
	// The day-grid component will render inside of a container defined by this HTML.
7479
	renderHtml: function() {
7480
		return '' +
7481
			'<table>' +
7482
				'<thead>' +
7483
					'<tr>' +
7484
						'<td class="' + this.widgetHeaderClass + '">' +
7485
							this.dayGrid.headHtml() + // render the day-of-week headers
7486
						'</td>' +
7487
					'</tr>' +
7488
				'</thead>' +
7489
				'<tbody>' +
7490
					'<tr>' +
7491
						'<td class="' + this.widgetContentClass + '">' +
7492
							'<div class="fc-day-grid-container">' +
7493
								'<div class="fc-day-grid"/>' +
7494
							'</div>' +
7495
						'</td>' +
7496
					'</tr>' +
7497
				'</tbody>' +
7498
			'</table>';
7499
	},
7500
7501
7502
	// Generates the HTML that will go before the day-of week header cells.
7503
	// Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL.
7504
	headIntroHtml: function() {
7505
		if (this.weekNumbersVisible) {
7506
			return '' +
7507
				'<th class="fc-week-number ' + this.widgetHeaderClass + '" ' + this.weekNumberStyleAttr() + '>' +
7508
					'<span>' + // needed for matchCellWidths
7509
						htmlEscape(this.opt('weekNumberTitle')) +
7510
					'</span>' +
7511
				'</th>';
7512
		}
7513
	},
7514
7515
7516
	// Generates the HTML that will go before content-skeleton cells that display the day/week numbers.
7517
	// Queried by the DayGrid subcomponent. Ordering depends on isRTL.
7518
	numberIntroHtml: function(row) {
7519
		if (this.weekNumbersVisible) {
7520
			return '' +
7521
				'<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '>' +
7522
					'<span>' + // needed for matchCellWidths
7523
						this.calendar.calculateWeekNumber(this.cellToDate(row, 0)) +
7524
					'</span>' +
7525
				'</td>';
7526
		}
7527
	},
7528
7529
7530
	// Generates the HTML that goes before the day bg cells for each day-row.
7531
	// Queried by the DayGrid subcomponent. Ordering depends on isRTL.
7532
	dayIntroHtml: function() {
7533
		if (this.weekNumbersVisible) {
7534
			return '<td class="fc-week-number ' + this.widgetContentClass + '" ' +
7535
				this.weekNumberStyleAttr() + '></td>';
7536
		}
7537
	},
7538
7539
7540
	// Generates the HTML that goes before every other type of row generated by DayGrid. Ordering depends on isRTL.
7541
	// Affects helper-skeleton and highlight-skeleton rows.
7542
	introHtml: function() {
7543
		if (this.weekNumbersVisible) {
7544
			return '<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '></td>';
7545
		}
7546
	},
7547
7548
7549
	// Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
7550
	// The number row will only exist if either day numbers or week numbers are turned on.
7551
	numberCellHtml: function(row, col, date) {
7552
		var classes;
7553
7554
		if (!this.dayNumbersVisible) { // if there are week numbers but not day numbers
7555
			return '<td/>'; //  will create an empty space above events :(
7556
		}
7557
7558
		classes = this.dayGrid.getDayClasses(date);
7559
		classes.unshift('fc-day-number');
7560
7561
		return '' +
7562
			'<td class="' + classes.join(' ') + '" data-date="' + date.format() + '">' +
7563
				date.date() +
7564
			'</td>';
7565
	},
7566
7567
7568
	// Generates an HTML attribute string for setting the width of the week number column, if it is known
7569
	weekNumberStyleAttr: function() {
7570
		if (this.weekNumberWidth !== null) {
7571
			return 'style="width:' + this.weekNumberWidth + 'px"';
7572
		}
7573
		return '';
7574
	},
7575
7576
7577
	// Determines whether each row should have a constant height
7578
	hasRigidRows: function() {
7579
		var eventLimit = this.opt('eventLimit');
7580
		return eventLimit && typeof eventLimit !== 'number';
7581
	},
7582
7583
7584
	/* Dimensions
7585
	------------------------------------------------------------------------------------------------------------------*/
7586
7587
7588
	// Refreshes the horizontal dimensions of the view
7589
	updateWidth: function() {
7590
		if (this.weekNumbersVisible) {
7591
			// Make sure all week number cells running down the side have the same width.
7592
			// Record the width for cells created later.
7593
			this.weekNumberWidth = matchCellWidths(
7594
				this.el.find('.fc-week-number')
7595
			);
7596
		}
7597
	},
7598
7599
7600
	// Adjusts the vertical dimensions of the view to the specified values
7601
	setHeight: function(totalHeight, isAuto) {
7602
		var eventLimit = this.opt('eventLimit');
7603
		var scrollerHeight;
7604
7605
		// reset all heights to be natural
7606
		unsetScroller(this.scrollerEl);
7607
		uncompensateScroll(this.headRowEl);
7608
7609
		this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed
7610
7611
		// is the event limit a constant level number?
7612
		if (eventLimit && typeof eventLimit === 'number') {
7613
			this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after
7614
		}
7615
7616
		scrollerHeight = this.computeScrollerHeight(totalHeight);
7617
		this.setGridHeight(scrollerHeight, isAuto);
7618
7619
		// is the event limit dynamically calculated?
7620
		if (eventLimit && typeof eventLimit !== 'number') {
7621
			this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set
7622
		}
7623
7624
		if (!isAuto && setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?
7625
7626
			compensateScroll(this.headRowEl, getScrollbarWidths(this.scrollerEl));
7627
7628
			// doing the scrollbar compensation might have created text overflow which created more height. redo
7629
			scrollerHeight = this.computeScrollerHeight(totalHeight);
7630
			this.scrollerEl.height(scrollerHeight);
7631
7632
			this.restoreScroll();
7633
		}
7634
	},
7635
7636
7637
	// Sets the height of just the DayGrid component in this view
7638
	setGridHeight: function(height, isAuto) {
7639
		if (isAuto) {
7640
			undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding
7641
		}
7642
		else {
7643
			distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows
7644
		}
7645
	},
7646
7647
7648
	/* Events
7649
	------------------------------------------------------------------------------------------------------------------*/
7650
7651
7652
	// Renders the given events onto the view and populates the segments array
7653
	renderEvents: function(events) {
7654
		this.dayGrid.renderEvents(events);
7655
7656
		this.updateHeight(); // must compensate for events that overflow the row
7657
7658
		View.prototype.renderEvents.call(this, events); // call the super-method
7659
	},
7660
7661
7662
	// Retrieves all segment objects that are rendered in the view
7663
	getSegs: function() {
7664
		return this.dayGrid.getSegs();
7665
	},
7666
7667
7668
	// Unrenders all event elements and clears internal segment data
7669
	destroyEvents: function() {
7670
		View.prototype.destroyEvents.call(this); // do this before dayGrid's segs have been cleared
7671
7672
		this.recordScroll(); // removing events will reduce height and mess with the scroll, so record beforehand
7673
		this.dayGrid.destroyEvents();
7674
7675
		// we DON'T need to call updateHeight() because:
7676
		// A) a renderEvents() call always happens after this, which will eventually call updateHeight()
7677
		// B) in IE8, this causes a flash whenever events are rerendered
7678
	},
7679
7680
7681
	/* Event Dragging
7682
	------------------------------------------------------------------------------------------------------------------*/
7683
7684
7685
	// Renders a visual indication of an event being dragged over the view.
7686
	// A returned value of `true` signals that a mock "helper" event has been rendered.
7687
	renderDrag: function(start, end, seg) {
7688
		return this.dayGrid.renderDrag(start, end, seg);
7689
	},
7690
7691
7692
	// Unrenders the visual indication of an event being dragged over the view
7693
	destroyDrag: function() {
7694
		this.dayGrid.destroyDrag();
7695
	},
7696
7697
7698
	/* Selection
7699
	------------------------------------------------------------------------------------------------------------------*/
7700
7701
7702
	// Renders a visual indication of a selection
7703
	renderSelection: function(start, end) {
7704
		this.dayGrid.renderSelection(start, end);
7705
	},
7706
7707
7708
	// Unrenders a visual indications of a selection
7709
	destroySelection: function() {
7710
		this.dayGrid.destroySelection();
7711
	}
7712
7713
});
7714
7715
;;
7716
7717
/* A month view with day cells running in rows (one-per-week) and columns
7718
----------------------------------------------------------------------------------------------------------------------*/
7719
7720
setDefaults({
7721
	fixedWeekCount: true
7722
});
7723
7724
fcViews.month = MonthView; // register the view
7725
7726
function MonthView(calendar) {
7727
	BasicView.call(this, calendar); // call the super-constructor
7728
}
7729
7730
7731
MonthView.prototype = createObject(BasicView.prototype); // define the super-class
7732
$.extend(MonthView.prototype, {
7733
7734
	name: 'month',
7735
7736
7737
	incrementDate: function(date, delta) {
7738
		return date.clone().stripTime().add(delta, 'months').startOf('month');
7739
	},
7740
7741
7742
	render: function(date) {
7743
		var rowCnt;
7744
7745
		this.intervalStart = date.clone().stripTime().startOf('month');
7746
		this.intervalEnd = this.intervalStart.clone().add(1, 'months');
7747
7748
		this.start = this.intervalStart.clone();
7749
		this.start = this.skipHiddenDays(this.start); // move past the first week if no visible days
7750
		this.start.startOf('week');
7751
		this.start = this.skipHiddenDays(this.start); // move past the first invisible days of the week
7752
7753
		this.end = this.intervalEnd.clone();
7754
		this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last week if no visible days
7755
		this.end.add((7 - this.end.weekday()) % 7, 'days'); // move to end of week if not already
7756
		this.end = this.skipHiddenDays(this.end, -1, true); // move in from the last invisible days of the week
7757
7758
		rowCnt = Math.ceil( // need to ceil in case there are hidden days
7759
			this.end.diff(this.start, 'weeks', true) // returnfloat=true
7760
		);
7761
		if (this.isFixedWeeks()) {
7762
			this.end.add(6 - rowCnt, 'weeks');
7763
			rowCnt = 6;
7764
		}
7765
7766
		this.title = this.calendar.formatDate(this.intervalStart, this.opt('titleFormat'));
7767
7768
		BasicView.prototype.render.call(this, rowCnt, this.getCellsPerWeek(), true); // call the super-method
7769
	},
7770
7771
7772
	// Overrides the default BasicView behavior to have special multi-week auto-height logic
7773
	setGridHeight: function(height, isAuto) {
7774
7775
		isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated
7776
7777
		// if auto, make the height of each row the height that it would be if there were 6 weeks
7778
		if (isAuto) {
7779
			height *= this.rowCnt / 6;
7780
		}
7781
7782
		distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows
7783
	},
7784
7785
7786
	isFixedWeeks: function() {
7787
		var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated
7788
		if (weekMode) {
7789
			return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed
7790
		}
7791
7792
		return this.opt('fixedWeekCount');
7793
	}
7794
7795
});
7796
7797
;;
7798
7799
/* A week view with simple day cells running horizontally
7800
----------------------------------------------------------------------------------------------------------------------*/
7801
// TODO: a WeekView mixin for calculating dates and titles
7802
7803
fcViews.basicWeek = BasicWeekView; // register this view
7804
7805
function BasicWeekView(calendar) {
7806
	BasicView.call(this, calendar); // call the super-constructor
7807
}
7808
7809
7810
BasicWeekView.prototype = createObject(BasicView.prototype); // define the super-class
7811
$.extend(BasicWeekView.prototype, {
7812
7813
	name: 'basicWeek',
7814
7815
7816
	incrementDate: function(date, delta) {
7817
		return date.clone().stripTime().add(delta, 'weeks').startOf('week');
7818
	},
7819
7820
7821
	render: function(date) {
7822
7823
		this.intervalStart = date.clone().stripTime().startOf('week');
7824
		this.intervalEnd = this.intervalStart.clone().add(1, 'weeks');
7825
7826
		this.start = this.skipHiddenDays(this.intervalStart);
7827
		this.end = this.skipHiddenDays(this.intervalEnd, -1, true);
7828
7829
		this.title = this.calendar.formatRange(
7830
			this.start,
7831
			this.end.clone().subtract(1), // make inclusive by subtracting 1 ms
7832
			this.opt('titleFormat'),
7833
			' \u2014 ' // emphasized dash
7834
		);
7835
7836
		BasicView.prototype.render.call(this, 1, this.getCellsPerWeek(), false); // call the super-method
7837
	}
7838
	
7839
});
7840
;;
7841
7842
/* A view with a single simple day cell
7843
----------------------------------------------------------------------------------------------------------------------*/
7844
7845
fcViews.basicDay = BasicDayView; // register this view
7846
7847
function BasicDayView(calendar) {
7848
	BasicView.call(this, calendar); // call the super-constructor
7849
}
7850
7851
7852
BasicDayView.prototype = createObject(BasicView.prototype); // define the super-class
7853
$.extend(BasicDayView.prototype, {
7854
7855
	name: 'basicDay',
7856
7857
7858
	incrementDate: function(date, delta) {
7859
		var out = date.clone().stripTime().add(delta, 'days');
7860
		out = this.skipHiddenDays(out, delta < 0 ? -1 : 1);
7861
		return out;
7862
	},
7863
7864
7865
	render: function(date) {
7866
7867
		this.start = this.intervalStart = date.clone().stripTime();
7868
		this.end = this.intervalEnd = this.start.clone().add(1, 'days');
7869
7870
		this.title = this.calendar.formatDate(this.start, this.opt('titleFormat'));
7871
7872
		BasicView.prototype.render.call(this, 1, 1, false); // call the super-method
7873
	}
7874
7875
});
7876
;;
7877
7878
/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically.
7879
----------------------------------------------------------------------------------------------------------------------*/
7880
// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).
7881
// Responsible for managing width/height.
7882
7883
setDefaults({
7884
	allDaySlot: true,
7885
	allDayText: 'all-day',
7886
7887
	scrollTime: '06:00:00',
7888
7889
	slotDuration: '00:30:00',
7890
7891
	axisFormat: generateAgendaAxisFormat,
7892
	timeFormat: {
7893
		agenda: generateAgendaTimeFormat
7894
	},
7895
7896
	minTime: '00:00:00',
7897
	maxTime: '24:00:00',
7898
	slotEventOverlap: true
7899
});
7900
7901
var AGENDA_ALL_DAY_EVENT_LIMIT = 5;
7902
7903
7904
function generateAgendaAxisFormat(options, langData) {
7905
	return langData.longDateFormat('LT')
7906
		.replace(':mm', '(:mm)')
7907
		.replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
7908
		.replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
7909
}
7910
7911
7912
function generateAgendaTimeFormat(options, langData) {
7913
	return langData.longDateFormat('LT')
7914
		.replace(/\s*a$/i, ''); // remove trailing AM/PM
7915
}
7916
7917
7918
function AgendaView(calendar) {
7919
	View.call(this, calendar); // call the super-constructor
7920
7921
	this.timeGrid = new TimeGrid(this);
7922
7923
	if (this.opt('allDaySlot')) { // should we display the "all-day" area?
7924
		this.dayGrid = new DayGrid(this); // the all-day subcomponent of this view
7925
7926
		// the coordinate grid will be a combination of both subcomponents' grids
7927
		this.coordMap = new ComboCoordMap([
7928
			this.dayGrid.coordMap,
7929
			this.timeGrid.coordMap
7930
		]);
7931
	}
7932
	else {
7933
		this.coordMap = this.timeGrid.coordMap;
7934
	}
7935
}
7936
7937
7938
AgendaView.prototype = createObject(View.prototype); // define the super-class
7939
$.extend(AgendaView.prototype, {
7940
7941
	timeGrid: null, // the main time-grid subcomponent of this view
7942
	dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null
7943
7944
	axisWidth: null, // the width of the time axis running down the side
7945
7946
	noScrollRowEls: null, // set of fake row elements that must compensate when scrollerEl has scrollbars
7947
7948
	// when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath
7949
	bottomRuleEl: null,
7950
	bottomRuleHeight: null,
7951
7952
7953
	/* Rendering
7954
	------------------------------------------------------------------------------------------------------------------*/
7955
7956
7957
	// Renders the view into `this.el`, which has already been assigned.
7958
	// `colCnt` has been calculated by a subclass and passed here.
7959
	render: function(colCnt) {
7960
7961
		// needed for cell-to-date and date-to-cell calculations in View
7962
		this.rowCnt = 1;
7963
		this.colCnt = colCnt;
7964
7965
		this.el.addClass('fc-agenda-view').html(this.renderHtml());
7966
7967
		// the element that wraps the time-grid that will probably scroll
7968
		this.scrollerEl = this.el.find('.fc-time-grid-container');
7969
		this.timeGrid.coordMap.containerEl = this.scrollerEl; // don't accept clicks/etc outside of this
7970
7971
		this.timeGrid.el = this.el.find('.fc-time-grid');
7972
		this.timeGrid.render();
7973
7974
		// the <hr> that sometimes displays under the time-grid
7975
		this.bottomRuleEl = $('<hr class="' + this.widgetHeaderClass + '"/>')
7976
			.appendTo(this.timeGrid.el); // inject it into the time-grid
7977
7978
		if (this.dayGrid) {
7979
			this.dayGrid.el = this.el.find('.fc-day-grid');
7980
			this.dayGrid.render();
7981
7982
			// have the day-grid extend it's coordinate area over the <hr> dividing the two grids
7983
			this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight();
7984
		}
7985
7986
		this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller
7987
7988
		View.prototype.render.call(this); // call the super-method
7989
7990
		this.resetScroll(); // do this after sizes have been set
7991
	},
7992
7993
7994
	// Make subcomponents ready for cleanup
7995
	destroy: function() {
7996
		this.timeGrid.destroy();
7997
		if (this.dayGrid) {
7998
			this.dayGrid.destroy();
7999
		}
8000
		View.prototype.destroy.call(this); // call the super-method
8001
	},
8002
8003
8004
	// Builds the HTML skeleton for the view.
8005
	// The day-grid and time-grid components will render inside containers defined by this HTML.
8006
	renderHtml: function() {
8007
		return '' +
8008
			'<table>' +
8009
				'<thead>' +
8010
					'<tr>' +
8011
						'<td class="' + this.widgetHeaderClass + '">' +
8012
							this.timeGrid.headHtml() + // render the day-of-week headers
8013
						'</td>' +
8014
					'</tr>' +
8015
				'</thead>' +
8016
				'<tbody>' +
8017
					'<tr>' +
8018
						'<td class="' + this.widgetContentClass + '">' +
8019
							(this.dayGrid ?
8020
								'<div class="fc-day-grid"/>' +
8021
								'<hr class="' + this.widgetHeaderClass + '"/>' :
8022
								''
8023
								) +
8024
							'<div class="fc-time-grid-container">' +
8025
								'<div class="fc-time-grid"/>' +
8026
							'</div>' +
8027
						'</td>' +
8028
					'</tr>' +
8029
				'</tbody>' +
8030
			'</table>';
8031
	},
8032
8033
8034
	// Generates the HTML that will go before the day-of week header cells.
8035
	// Queried by the TimeGrid subcomponent when generating rows. Ordering depends on isRTL.
8036
	headIntroHtml: function() {
8037
		var date;
8038
		var weekNumber;
8039
		var weekTitle;
8040
		var weekText;
8041
8042
		if (this.opt('weekNumbers')) {
8043
			date = this.cellToDate(0, 0);
8044
			weekNumber = this.calendar.calculateWeekNumber(date);
8045
			weekTitle = this.opt('weekNumberTitle');
8046
8047
			if (this.opt('isRTL')) {
8048
				weekText = weekNumber + weekTitle;
8049
			}
8050
			else {
8051
				weekText = weekTitle + weekNumber;
8052
			}
8053
8054
			return '' +
8055
				'<th class="fc-axis fc-week-number ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '>' +
8056
					'<span>' + // needed for matchCellWidths
8057
						htmlEscape(weekText) +
8058
					'</span>' +
8059
				'</th>';
8060
		}
8061
		else {
8062
			return '<th class="fc-axis ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '></th>';
8063
		}
8064
	},
8065
8066
8067
	// Generates the HTML that goes before the all-day cells.
8068
	// Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL.
8069
	dayIntroHtml: function() {
8070
		return '' +
8071
			'<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '>' +
8072
				'<span>' + // needed for matchCellWidths
8073
					(this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))) +
8074
				'</span>' +
8075
			'</td>';
8076
	},
8077
8078
8079
	// Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
8080
	slotBgIntroHtml: function() {
8081
		return '<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '></td>';
8082
	},
8083
8084
8085
	// Generates the HTML that goes before all other types of cells.
8086
	// Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
8087
	// Queried by the TimeGrid and DayGrid subcomponents when generating rows. Ordering depends on isRTL.
8088
	introHtml: function() {
8089
		return '<td class="fc-axis" ' + this.axisStyleAttr() + '></td>';
8090
	},
8091
8092
8093
	// Generates an HTML attribute string for setting the width of the axis, if it is known
8094
	axisStyleAttr: function() {
8095
		if (this.axisWidth !== null) {
8096
			 return 'style="width:' + this.axisWidth + 'px"';
8097
		}
8098
		return '';
8099
	},
8100
8101
8102
	/* Dimensions
8103
	------------------------------------------------------------------------------------------------------------------*/
8104
8105
	updateSize: function(isResize) {
8106
		if (isResize) {
8107
			this.timeGrid.resize();
8108
		}
8109
		View.prototype.updateSize.call(this, isResize);
8110
	},
8111
8112
8113
	// Refreshes the horizontal dimensions of the view
8114
	updateWidth: function() {
8115
		// make all axis cells line up, and record the width so newly created axis cells will have it
8116
		this.axisWidth = matchCellWidths(this.el.find('.fc-axis'));
8117
	},
8118
8119
8120
	// Adjusts the vertical dimensions of the view to the specified values
8121
	setHeight: function(totalHeight, isAuto) {
8122
		var eventLimit;
8123
		var scrollerHeight;
8124
8125
		if (this.bottomRuleHeight === null) {
8126
			// calculate the height of the rule the very first time
8127
			this.bottomRuleHeight = this.bottomRuleEl.outerHeight();
8128
		}
8129
		this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary
8130
8131
		// reset all dimensions back to the original state
8132
		this.scrollerEl.css('overflow', '');
8133
		unsetScroller(this.scrollerEl);
8134
		uncompensateScroll(this.noScrollRowEls);
8135
8136
		// limit number of events in the all-day area
8137
		if (this.dayGrid) {
8138
			this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed
8139
8140
			eventLimit = this.opt('eventLimit');
8141
			if (eventLimit && typeof eventLimit !== 'number') {
8142
				eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number
8143
			}
8144
			if (eventLimit) {
8145
				this.dayGrid.limitRows(eventLimit);
8146
			}
8147
		}
8148
8149
		if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height?
8150
8151
			scrollerHeight = this.computeScrollerHeight(totalHeight);
8152
			if (setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?
8153
8154
				// make the all-day and header rows lines up
8155
				compensateScroll(this.noScrollRowEls, getScrollbarWidths(this.scrollerEl));
8156
8157
				// the scrollbar compensation might have changed text flow, which might affect height, so recalculate
8158
				// and reapply the desired height to the scroller.
8159
				scrollerHeight = this.computeScrollerHeight(totalHeight);
8160
				this.scrollerEl.height(scrollerHeight);
8161
8162
				this.restoreScroll();
8163
			}
8164
			else { // no scrollbars
8165
				// still, force a height and display the bottom rule (marks the end of day)
8166
				this.scrollerEl.height(scrollerHeight).css('overflow', 'hidden'); // in case <hr> goes outside
8167
				this.bottomRuleEl.show();
8168
			}
8169
		}
8170
	},
8171
8172
8173
	// Sets the scroll value of the scroller to the intial pre-configured state prior to allowing the user to change it.
8174
	resetScroll: function() {
8175
		var _this = this;
8176
		var scrollTime = moment.duration(this.opt('scrollTime'));
8177
		var top = this.timeGrid.computeTimeTop(scrollTime);
8178
8179
		// zoom can give weird floating-point values. rather scroll a little bit further
8180
		top = Math.ceil(top);
8181
8182
		if (top) {
8183
			top++; // to overcome top border that slots beyond the first have. looks better
8184
		}
8185
8186
		function scroll() {
8187
			_this.scrollerEl.scrollTop(top);
8188
		}
8189
8190
		scroll();
8191
		setTimeout(scroll, 0); // overrides any previous scroll state made by the browser
8192
	},
8193
8194
8195
	/* Events
8196
	------------------------------------------------------------------------------------------------------------------*/
8197
8198
8199
	// Renders events onto the view and populates the View's segment array
8200
	renderEvents: function(events) {
8201
		var dayEvents = [];
8202
		var timedEvents = [];
8203
		var daySegs = [];
0 ignored issues
show
Unused Code introduced by
The variable daySegs seems to be never used. Consider removing it.
Loading history...
8204
		var timedSegs;
8205
		var i;
8206
8207
		// separate the events into all-day and timed
8208
		for (i = 0; i < events.length; i++) {
8209
			if (events[i].allDay) {
8210
				dayEvents.push(events[i]);
8211
			}
8212
			else {
8213
				timedEvents.push(events[i]);
8214
			}
8215
		}
8216
8217
		// render the events in the subcomponents
8218
		timedSegs = this.timeGrid.renderEvents(timedEvents);
0 ignored issues
show
Unused Code introduced by
The variable timedSegs seems to be never used. Consider removing it.
Loading history...
8219
		if (this.dayGrid) {
8220
			daySegs = this.dayGrid.renderEvents(dayEvents);
8221
		}
8222
8223
		// the all-day area is flexible and might have a lot of events, so shift the height
8224
		this.updateHeight();
8225
8226
		View.prototype.renderEvents.call(this, events); // call the super-method
8227
	},
8228
8229
8230
	// Retrieves all segment objects that are rendered in the view
8231
	getSegs: function() {
8232
		return this.timeGrid.getSegs().concat(
8233
			this.dayGrid ? this.dayGrid.getSegs() : []
8234
		);
8235
	},
8236
8237
8238
	// Unrenders all event elements and clears internal segment data
8239
	destroyEvents: function() {
8240
		View.prototype.destroyEvents.call(this); // do this before the grids' segs have been cleared
8241
8242
		// if destroyEvents is being called as part of an event rerender, renderEvents will be called shortly
8243
		// after, so remember what the scroll value was so we can restore it.
8244
		this.recordScroll();
8245
8246
		// destroy the events in the subcomponents
8247
		this.timeGrid.destroyEvents();
8248
		if (this.dayGrid) {
8249
			this.dayGrid.destroyEvents();
8250
		}
8251
8252
		// we DON'T need to call updateHeight() because:
8253
		// A) a renderEvents() call always happens after this, which will eventually call updateHeight()
8254
		// B) in IE8, this causes a flash whenever events are rerendered
8255
	},
8256
8257
8258
	/* Event Dragging
8259
	------------------------------------------------------------------------------------------------------------------*/
8260
8261
8262
	// Renders a visual indication of an event being dragged over the view.
8263
	// A returned value of `true` signals that a mock "helper" event has been rendered.
8264
	renderDrag: function(start, end, seg) {
8265
		if (start.hasTime()) {
8266
			return this.timeGrid.renderDrag(start, end, seg);
8267
		}
8268
		else if (this.dayGrid) {
8269
			return this.dayGrid.renderDrag(start, end, seg);
8270
		}
8271
	},
8272
8273
8274
	// Unrenders a visual indications of an event being dragged over the view
8275
	destroyDrag: function() {
8276
		this.timeGrid.destroyDrag();
8277
		if (this.dayGrid) {
8278
			this.dayGrid.destroyDrag();
8279
		}
8280
	},
8281
8282
8283
	/* Selection
8284
	------------------------------------------------------------------------------------------------------------------*/
8285
8286
8287
	// Renders a visual indication of a selection
8288
	renderSelection: function(start, end) {
8289
		if (start.hasTime() || end.hasTime()) {
8290
			this.timeGrid.renderSelection(start, end);
8291
		}
8292
		else if (this.dayGrid) {
8293
			this.dayGrid.renderSelection(start, end);
8294
		}
8295
	},
8296
8297
8298
	// Unrenders a visual indications of a selection
8299
	destroySelection: function() {
8300
		this.timeGrid.destroySelection();
8301
		if (this.dayGrid) {
8302
			this.dayGrid.destroySelection();
8303
		}
8304
	}
8305
8306
});
8307
8308
;;
8309
8310
/* A week view with an all-day cell area at the top, and a time grid below
8311
----------------------------------------------------------------------------------------------------------------------*/
8312
// TODO: a WeekView mixin for calculating dates and titles
8313
8314
fcViews.agendaWeek = AgendaWeekView; // register the view
8315
8316
function AgendaWeekView(calendar) {
8317
	AgendaView.call(this, calendar); // call the super-constructor
8318
}
8319
8320
8321
AgendaWeekView.prototype = createObject(AgendaView.prototype); // define the super-class
8322
$.extend(AgendaWeekView.prototype, {
8323
8324
	name: 'agendaWeek',
8325
8326
8327
	incrementDate: function(date, delta) {
8328
		return date.clone().stripTime().add(delta, 'weeks').startOf('week');
8329
	},
8330
8331
8332
	render: function(date) {
8333
8334
		this.intervalStart = date.clone().stripTime().startOf('week');
8335
		this.intervalEnd = this.intervalStart.clone().add(1, 'weeks');
8336
8337
		this.start = this.skipHiddenDays(this.intervalStart);
8338
		this.end = this.skipHiddenDays(this.intervalEnd, -1, true);
8339
8340
		this.title = this.calendar.formatRange(
8341
			this.start,
8342
			this.end.clone().subtract(1), // make inclusive by subtracting 1 ms
8343
			this.opt('titleFormat'),
8344
			' \u2014 ' // emphasized dash
8345
		);
8346
8347
		AgendaView.prototype.render.call(this, this.getCellsPerWeek()); // call the super-method
8348
	}
8349
8350
});
8351
8352
;;
8353
8354
/* A day view with an all-day cell area at the top, and a time grid below
8355
----------------------------------------------------------------------------------------------------------------------*/
8356
8357
fcViews.agendaDay = AgendaDayView; // register the view
8358
8359
function AgendaDayView(calendar) {
8360
	AgendaView.call(this, calendar); // call the super-constructor
8361
}
8362
8363
8364
AgendaDayView.prototype = createObject(AgendaView.prototype); // define the super-class
8365
$.extend(AgendaDayView.prototype, {
8366
8367
	name: 'agendaDay',
8368
8369
8370
	incrementDate: function(date, delta) {
8371
		var out = date.clone().stripTime().add(delta, 'days');
8372
		out = this.skipHiddenDays(out, delta < 0 ? -1 : 1);
8373
		return out;
8374
	},
8375
8376
8377
	render: function(date) {
8378
8379
		this.start = this.intervalStart = date.clone().stripTime();
8380
		this.end = this.intervalEnd = this.start.clone().add(1, 'days');
8381
8382
		this.title = this.calendar.formatDate(this.start, this.opt('titleFormat'));
8383
8384
		AgendaView.prototype.render.call(this, 1); // call the super-method
8385
	}
8386
8387
});
8388
8389
;;
8390
8391
});